From 00e49472a7426d7cbee009531e12fba5a7c96d5b Mon Sep 17 00:00:00 2001 From: Conny Duck Date: Wed, 25 May 2022 19:33:24 +0200 Subject: [PATCH 01/12] fix conversations --- .../conversation/ConversationAdapter.kt | 4 +- .../conversation/ConversationEntity.kt | 9 +++- .../conversation/ConversationViewData.kt | 2 + .../conversation/ConversationViewHolder.java | 16 +++---- .../conversation/ConversationsFragment.kt | 48 +++++++++++-------- .../ConversationsRemoteMediator.kt | 47 +++++++++++++----- .../conversation/ConversationsViewModel.kt | 2 +- .../tusky/db/ConversationsDao.kt | 6 +-- .../tusky/network/MastodonApi.kt | 4 +- 9 files changed, 89 insertions(+), 49 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationAdapter.kt index 0c94651420..4aa1a54b34 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationAdapter.kt @@ -34,7 +34,9 @@ class ConversationAdapter( } override fun onBindViewHolder(holder: ConversationViewHolder, position: Int) { - holder.setupWithConversation(getItem(position)) + getItem(position)?.let { conversationViewData -> + holder.setupWithConversation(conversationViewData) + } } companion object { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt index 5462ea7b5b..401d61463d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt @@ -34,6 +34,7 @@ import java.util.Date data class ConversationEntity( val accountId: Long, val id: String, + val order: Int, val accounts: List, val unread: Boolean, @Embedded(prefix = "s_") val lastStatus: ConversationStatusEntity @@ -41,6 +42,7 @@ data class ConversationEntity( fun toViewData(): ConversationViewData { return ConversationViewData( id = id, + order = order, accounts = accounts, unread = unread, lastStatus = lastStatus.toViewData() @@ -50,6 +52,7 @@ data class ConversationEntity( data class ConversationAccountEntity( val id: String, + val localUsername: String, val username: String, val displayName: String, val avatar: String, @@ -58,12 +61,12 @@ data class ConversationAccountEntity( fun toAccount(): TimelineAccount { return TimelineAccount( id = id, + localUsername = localUsername, username = username, displayName = displayName, url = "", avatar = avatar, emojis = emojis, - localUsername = "", ) } } @@ -134,6 +137,7 @@ data class ConversationStatusEntity( fun TimelineAccount.toEntity() = ConversationAccountEntity( id = id, + localUsername = localUsername, username = username, displayName = name, avatar = avatar, @@ -166,10 +170,11 @@ fun Status.toEntity() = poll = poll ) -fun Conversation.toEntity(accountId: Long) = +fun Conversation.toEntity(accountId: Long, order: Int) = ConversationEntity( accountId = accountId, id = id, + order = order, accounts = accounts.map { it.toEntity() }, unread = unread, lastStatus = lastStatus!!.toEntity() diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewData.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewData.kt index d63fce6c1c..fae55f0ba6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewData.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewData.kt @@ -20,6 +20,7 @@ import com.keylesspalace.tusky.viewdata.StatusViewData data class ConversationViewData( val id: String, + val order: Int, val accounts: List, val unread: Boolean, val lastStatus: StatusViewData.Concrete @@ -37,6 +38,7 @@ data class ConversationViewData( return ConversationEntity( accountId = accountId, id = id, + order = order, accounts = accounts, unread = unread, lastStatus = lastStatus.toConversationStatusEntity( diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java index ffb88a942e..addc38513e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java @@ -23,6 +23,7 @@ import android.widget.ImageView; import android.widget.TextView; +import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.keylesspalace.tusky.R; @@ -43,12 +44,12 @@ public class ConversationViewHolder extends StatusBaseViewHolder { private static final InputFilter[] COLLAPSE_INPUT_FILTER = new InputFilter[]{SmartLengthInputFilter.INSTANCE}; private static final InputFilter[] NO_INPUT_FILTER = new InputFilter[0]; - private TextView conversationNameTextView; - private Button contentCollapseButton; - private ImageView[] avatars; + private final TextView conversationNameTextView; + private final Button contentCollapseButton; + private final ImageView[] avatars; - private StatusDisplayOptions statusDisplayOptions; - private StatusActionListener listener; + private final StatusDisplayOptions statusDisplayOptions; + private final StatusActionListener listener; ConversationViewHolder(View itemView, StatusDisplayOptions statusDisplayOptions, @@ -64,7 +65,6 @@ public class ConversationViewHolder extends StatusBaseViewHolder { this.statusDisplayOptions = statusDisplayOptions; this.listener = listener; - } @Override @@ -72,7 +72,7 @@ protected int getMediaPreviewHeight(Context context) { return context.getResources().getDimensionPixelSize(R.dimen.status_media_preview_height); } - void setupWithConversation(ConversationViewData conversation) { + void setupWithConversation(@NonNull ConversationViewData conversation) { StatusViewData.Concrete statusViewData = conversation.getLastStatus(); Status status = statusViewData.getStatus(); TimelineAccount account = status.getAccount(); @@ -169,4 +169,4 @@ private void setupCollapsedState(boolean collapsible, boolean collapsed, boolean content.setFilters(NO_INPUT_FILTER); } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt index 243c37448e..51d1096245 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt @@ -23,12 +23,13 @@ import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.PopupMenu import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope -import androidx.paging.ExperimentalPagingApi import androidx.paging.LoadState import androidx.preference.PreferenceManager import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.SimpleItemAnimator +import at.connyduck.sparkbutton.helpers.Utils import com.keylesspalace.tusky.R import com.keylesspalace.tusky.StatusListActivity import com.keylesspalace.tusky.components.account.AccountActivity @@ -51,7 +52,6 @@ import kotlinx.coroutines.launch import java.io.IOException import javax.inject.Inject -@OptIn(ExperimentalPagingApi::class) class ConversationsFragment : SFragment(), StatusActionListener, Injectable, ReselectableFragment { @Inject @@ -139,6 +139,18 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res } } } + + adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { + if (positionStart == 0 && adapter.itemCount != itemCount) { + binding.recyclerView.post { + if (getView() != null) { + binding.recyclerView.scrollBy(0, Utils.dpToPx(requireContext(), -30)) + } + } + } + } + }) } private fun initSwipeToRefresh() { @@ -201,7 +213,7 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res } override fun onOpenReblog(position: Int) { - // there are no reblogs in search results + // there are no reblogs in conversations } override fun onExpandedChange(expanded: Boolean, position: Int) { @@ -246,31 +258,27 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res } } - private fun deleteConversation(conversation: ConversationViewData) { - AlertDialog.Builder(requireContext()) - .setMessage(R.string.dialog_delete_conversation_warning) - .setNegativeButton(android.R.string.cancel, null) - .setPositiveButton(android.R.string.ok) { _, _ -> - viewModel.remove(conversation) - } - .show() + override fun onVoteInPoll(position: Int, choices: MutableList) { + adapter.peek(position)?.let { conversation -> + viewModel.voteInPoll(choices, conversation) + } } - private fun jumpToTop() { + override fun onReselect() { if (isAdded) { layoutManager?.scrollToPosition(0) binding.recyclerView.stopScroll() } } - override fun onReselect() { - jumpToTop() - } - - override fun onVoteInPoll(position: Int, choices: MutableList) { - adapter.peek(position)?.let { conversation -> - viewModel.voteInPoll(choices, conversation) - } + private fun deleteConversation(conversation: ConversationViewData) { + AlertDialog.Builder(requireContext()) + .setMessage(R.string.dialog_delete_conversation_warning) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(android.R.string.ok) { _, _ -> + viewModel.remove(conversation) + } + .show() } companion object { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRemoteMediator.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRemoteMediator.kt index 26984c8e88..e03cefaed3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRemoteMediator.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRemoteMediator.kt @@ -4,8 +4,11 @@ import androidx.paging.ExperimentalPagingApi import androidx.paging.LoadType import androidx.paging.PagingState import androidx.paging.RemoteMediator +import androidx.room.withTransaction import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.HttpHeaderLink +import retrofit2.HttpException @OptIn(ExperimentalPagingApi::class) class ConversationsRemoteMediator( @@ -14,34 +17,54 @@ class ConversationsRemoteMediator( private val db: AppDatabase ) : RemoteMediator() { + private var nextKey: String? = null + + private var order: Int = 0 + override suspend fun load( loadType: LoadType, state: PagingState ): MediatorResult { try { - val conversationsResult = when (loadType) { + val conversationsResponse = when (loadType) { LoadType.REFRESH -> { - api.getConversations(limit = state.config.initialLoadSize) + nextKey = null + order = 0 + api.getConversations(limit = state.config.pageSize) } LoadType.PREPEND -> { return MediatorResult.Success(endOfPaginationReached = true) } LoadType.APPEND -> { - val maxId = state.pages.findLast { it.data.isNotEmpty() }?.data?.lastOrNull()?.lastStatus?.id - api.getConversations(maxId = maxId, limit = state.config.pageSize) + api.getConversations(maxId = nextKey, limit = state.config.pageSize) } } - if (loadType == LoadType.REFRESH) { - db.conversationDao().deleteForAccount(accountId) + val conversations = conversationsResponse.body() + if (!conversationsResponse.isSuccessful || conversations == null) { + return MediatorResult.Error(HttpException(conversationsResponse)) + } + + db.withTransaction { + + if (loadType == LoadType.REFRESH) { + db.conversationDao().deleteForAccount(accountId) + } + + val linkHeader = conversationsResponse.headers()["Link"] + val links = HttpHeaderLink.parse(linkHeader) + nextKey = HttpHeaderLink.findByRelationType(links, "next")?.uri?.getQueryParameter("max_id") + + db.conversationDao().insert( + conversations + .filterNot { it.lastStatus == null } + .map { + it.toEntity(accountId, order++) + } + ) } - db.conversationDao().insert( - conversationsResult - .filterNot { it.lastStatus == null } - .map { it.toEntity(accountId) } - ) - return MediatorResult.Success(endOfPaginationReached = conversationsResult.isEmpty()) + return MediatorResult.Success(endOfPaginationReached = nextKey == null) } catch (e: Exception) { return MediatorResult.Error(e) } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt index 9326a05c08..61ab8eb4f3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt @@ -41,7 +41,7 @@ class ConversationsViewModel @Inject constructor( @OptIn(ExperimentalPagingApi::class) val conversationFlow = Pager( - config = PagingConfig(pageSize = 10, enablePlaceholders = false, initialLoadSize = 20), + config = PagingConfig(pageSize = 20), remoteMediator = ConversationsRemoteMediator(accountManager.activeAccount!!.id, api, database), pagingSourceFactory = { database.conversationDao().conversationsForAccount(accountManager.activeAccount!!.id) } ) diff --git a/app/src/main/java/com/keylesspalace/tusky/db/ConversationsDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/ConversationsDao.kt index fe093bd0c6..d650d42da1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/ConversationsDao.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/ConversationsDao.kt @@ -28,12 +28,12 @@ interface ConversationsDao { suspend fun insert(conversations: List) @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insert(conversation: ConversationEntity): Long + suspend fun insert(conversation: ConversationEntity) @Query("DELETE FROM ConversationEntity WHERE id = :id AND accountId = :accountId") - suspend fun delete(id: String, accountId: Long): Int + suspend fun delete(id: String, accountId: Long) - @Query("SELECT * FROM ConversationEntity WHERE accountId = :accountId ORDER BY s_createdAt DESC") + @Query("SELECT * FROM ConversationEntity WHERE accountId = :accountId ORDER BY `order` ASC") fun conversationsForAccount(accountId: Long): PagingSource @Query("DELETE FROM ConversationEntity WHERE accountId = :accountId") diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt index 0d9a1945c3..abd3c1244a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -503,8 +503,8 @@ interface MastodonApi { @GET("/api/v1/conversations") suspend fun getConversations( @Query("max_id") maxId: String? = null, - @Query("limit") limit: Int - ): List + @Query("limit") limit: Int? = null + ): Response> @DELETE("/api/v1/conversations/{id}") suspend fun deleteConversation( From 01f81b399775bb7236e314356f22fa8365d965fa Mon Sep 17 00:00:00 2001 From: Conny Duck Date: Wed, 25 May 2022 19:42:26 +0200 Subject: [PATCH 02/12] cleanup ConversationsRemoteMediator --- .../ConversationsRemoteMediator.kt | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRemoteMediator.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRemoteMediator.kt index e03cefaed3..02a44f951f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRemoteMediator.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRemoteMediator.kt @@ -26,20 +26,17 @@ class ConversationsRemoteMediator( state: PagingState ): MediatorResult { + if (loadType == LoadType.PREPEND) { + return MediatorResult.Success(endOfPaginationReached = true) + } + + if (loadType == LoadType.REFRESH) { + nextKey = null + order = 0 + } + try { - val conversationsResponse = when (loadType) { - LoadType.REFRESH -> { - nextKey = null - order = 0 - api.getConversations(limit = state.config.pageSize) - } - LoadType.PREPEND -> { - return MediatorResult.Success(endOfPaginationReached = true) - } - LoadType.APPEND -> { - api.getConversations(maxId = nextKey, limit = state.config.pageSize) - } - } + val conversationsResponse = api.getConversations(maxId = nextKey, limit = state.config.pageSize) val conversations = conversationsResponse.body() if (!conversationsResponse.isSuccessful || conversations == null) { @@ -69,6 +66,4 @@ class ConversationsRemoteMediator( return MediatorResult.Error(e) } } - - override suspend fun initialize() = InitializeAction.LAUNCH_INITIAL_REFRESH } From 76020c1b36f88e8e4f9535b21c1ece48ecec856e Mon Sep 17 00:00:00 2001 From: Conny Duck Date: Wed, 25 May 2022 20:36:54 +0200 Subject: [PATCH 03/12] update conversation timestamps regularly --- .../conversation/ConversationAdapter.kt | 23 ++++- .../conversation/ConversationViewHolder.java | 92 +++++++++++-------- .../conversation/ConversationsFragment.kt | 12 +++ 3 files changed, 87 insertions(+), 40 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationAdapter.kt index 4aa1a54b34..683f16d270 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationAdapter.kt @@ -20,6 +20,7 @@ import android.view.ViewGroup import androidx.paging.PagingDataAdapter import androidx.recyclerview.widget.DiffUtil import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.adapter.StatusBaseViewHolder import com.keylesspalace.tusky.interfaces.StatusActionListener import com.keylesspalace.tusky.util.StatusDisplayOptions @@ -34,8 +35,16 @@ class ConversationAdapter( } override fun onBindViewHolder(holder: ConversationViewHolder, position: Int) { + onBindViewHolder(holder, position, emptyList()) + } + + override fun onBindViewHolder( + holder: ConversationViewHolder, + position: Int, + payloads: List + ) { getItem(position)?.let { conversationViewData -> - holder.setupWithConversation(conversationViewData) + holder.setupWithConversation(conversationViewData, payloads.firstOrNull()) } } @@ -46,7 +55,17 @@ class ConversationAdapter( } override fun areContentsTheSame(oldItem: ConversationViewData, newItem: ConversationViewData): Boolean { - return oldItem == newItem + return false // Items are different always. It allows to refresh timestamp on every view holder update + } + + override fun getChangePayload(oldItem: ConversationViewData, newItem: ConversationViewData): Any? { + return if (oldItem == newItem) { + // If items are equal - update timestamp only + listOf(StatusBaseViewHolder.Key.KEY_CREATED) + } else { + // If items are different - update the whole view holder + null + } } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java index addc38513e..19280441eb 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java @@ -24,6 +24,7 @@ import android.widget.TextView; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; import com.keylesspalace.tusky.R; @@ -72,52 +73,67 @@ protected int getMediaPreviewHeight(Context context) { return context.getResources().getDimensionPixelSize(R.dimen.status_media_preview_height); } - void setupWithConversation(@NonNull ConversationViewData conversation) { + void setupWithConversation( + @NonNull ConversationViewData conversation, + @Nullable Object payloads + ) { + StatusViewData.Concrete statusViewData = conversation.getLastStatus(); Status status = statusViewData.getStatus(); - TimelineAccount account = status.getAccount(); - - setupCollapsedState(statusViewData.isCollapsible(), statusViewData.isCollapsed(), statusViewData.isExpanded(), statusViewData.getSpoilerText(), listener); - - setDisplayName(account.getDisplayName(), account.getEmojis(), statusDisplayOptions); - setUsername(account.getUsername()); - setCreatedAt(status.getCreatedAt(), statusDisplayOptions); - setIsReply(status.getInReplyToId() != null); - setFavourited(status.getFavourited()); - setBookmarked(status.getBookmarked()); - List attachments = status.getAttachments(); - boolean sensitive = status.getSensitive(); - if (statusDisplayOptions.mediaPreviewEnabled() && hasPreviewableAttachment(attachments)) { - setMediaPreviews(attachments, sensitive, listener, statusViewData.isShowingContent(), - statusDisplayOptions.useBlurhash()); - - if (attachments.size() == 0) { + + if (payloads == null) { + TimelineAccount account = status.getAccount(); + + setupCollapsedState(statusViewData.isCollapsible(), statusViewData.isCollapsed(), statusViewData.isExpanded(), statusViewData.getSpoilerText(), listener); + + setDisplayName(account.getDisplayName(), account.getEmojis(), statusDisplayOptions); + setUsername(account.getUsername()); + setCreatedAt(status.getCreatedAt(), statusDisplayOptions); + setIsReply(status.getInReplyToId() != null); + setFavourited(status.getFavourited()); + setBookmarked(status.getBookmarked()); + List attachments = status.getAttachments(); + boolean sensitive = status.getSensitive(); + if (statusDisplayOptions.mediaPreviewEnabled() && hasPreviewableAttachment(attachments)) { + setMediaPreviews(attachments, sensitive, listener, statusViewData.isShowingContent(), + statusDisplayOptions.useBlurhash()); + + if (attachments.size() == 0) { + hideSensitiveMediaWarning(); + } + // Hide the unused label. + for (TextView mediaLabel : mediaLabels) { + mediaLabel.setVisibility(View.GONE); + } + } else { + setMediaLabel(attachments, sensitive, listener, statusViewData.isShowingContent()); + // Hide all unused views. + mediaPreviews[0].setVisibility(View.GONE); + mediaPreviews[1].setVisibility(View.GONE); + mediaPreviews[2].setVisibility(View.GONE); + mediaPreviews[3].setVisibility(View.GONE); hideSensitiveMediaWarning(); } - // Hide the unused label. - for (TextView mediaLabel : mediaLabels) { - mediaLabel.setVisibility(View.GONE); - } - } else { - setMediaLabel(attachments, sensitive, listener, statusViewData.isShowingContent()); - // Hide all unused views. - mediaPreviews[0].setVisibility(View.GONE); - mediaPreviews[1].setVisibility(View.GONE); - mediaPreviews[2].setVisibility(View.GONE); - mediaPreviews[3].setVisibility(View.GONE); - hideSensitiveMediaWarning(); - } - setupButtons(listener, account.getId(), statusViewData.getContent().toString(), - statusDisplayOptions); + setupButtons(listener, account.getId(), statusViewData.getContent().toString(), + statusDisplayOptions); - setSpoilerAndContent(statusViewData.isExpanded(), statusViewData.getContent(), status.getSpoilerText(), - status.getMentions(), status.getTags(), status.getEmojis(), - PollViewDataKt.toViewData(status.getPoll()), statusDisplayOptions, listener); + setSpoilerAndContent(statusViewData.isExpanded(), statusViewData.getContent(), status.getSpoilerText(), + status.getMentions(), status.getTags(), status.getEmojis(), + PollViewDataKt.toViewData(status.getPoll()), statusDisplayOptions, listener); - setConversationName(conversation.getAccounts()); + setConversationName(conversation.getAccounts()); - setAvatars(conversation.getAccounts()); + setAvatars(conversation.getAccounts()); + } else { + if (payloads instanceof List) { + for (Object item : (List) payloads) { + if (Key.KEY_CREATED.equals(item)) { + setCreatedAt(status.getCreatedAt(), statusDisplayOptions); + } + } + } + } } private void setConversationName(List accounts) { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt index 51d1096245..172d819af6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt @@ -32,6 +32,7 @@ import androidx.recyclerview.widget.SimpleItemAnimator import at.connyduck.sparkbutton.helpers.Utils import com.keylesspalace.tusky.R import com.keylesspalace.tusky.StatusListActivity +import com.keylesspalace.tusky.adapter.StatusBaseViewHolder import com.keylesspalace.tusky.components.account.AccountActivity import com.keylesspalace.tusky.databinding.FragmentTimelineBinding import com.keylesspalace.tusky.di.Injectable @@ -47,10 +48,13 @@ import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.visible import com.keylesspalace.tusky.viewdata.AttachmentViewData +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import java.io.IOException import javax.inject.Inject +import kotlin.time.DurationUnit +import kotlin.time.toDuration class ConversationsFragment : SFragment(), StatusActionListener, Injectable, ReselectableFragment { @@ -151,6 +155,14 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res } } }) + + lifecycleScope.launchWhenResumed { + val useAbsoluteTime = preferences.getBoolean(PrefKeys.ABSOLUTE_TIME_VIEW, false) + while (!useAbsoluteTime) { + adapter.notifyItemRangeChanged(0, adapter.itemCount, listOf(StatusBaseViewHolder.Key.KEY_CREATED)) + delay(1.toDuration(DurationUnit.MINUTES)) + } + } } private fun initSwipeToRefresh() { From 42b87064d3e90444eab0c495cd449d6b70b5a922 Mon Sep 17 00:00:00 2001 From: Conny Duck Date: Wed, 25 May 2022 20:51:04 +0200 Subject: [PATCH 04/12] improve loadStateListener --- .../conversation/ConversationsFragment.kt | 51 +++++++++---------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt index 172d819af6..fd9035c506 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt @@ -46,7 +46,6 @@ import com.keylesspalace.tusky.util.StatusDisplayOptions import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.viewBinding -import com.keylesspalace.tusky.util.visible import com.keylesspalace.tusky.viewdata.AttachmentViewData import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest @@ -70,8 +69,6 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res private var layoutManager: LinearLayoutManager? = null - private var initialRefreshDone: Boolean = false - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return inflater.inflate(R.layout.fragment_timeline, container, false) } @@ -112,34 +109,34 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res } } - adapter.addLoadStateListener { loadStates -> + adapter.addLoadStateListener { loadState -> + if (loadState.refresh != LoadState.Loading && loadState.source.refresh != LoadState.Loading) { + binding.swipeRefreshLayout.isRefreshing = false + } - loadStates.refresh.let { refreshState -> - if (refreshState is LoadState.Error) { - binding.statusView.show() - if (refreshState.error is IOException) { - binding.statusView.setup(R.drawable.elephant_offline, R.string.error_network) { - adapter.refresh() - } - } else { - binding.statusView.setup(R.drawable.elephant_error, R.string.error_generic) { - adapter.refresh() + binding.statusView.hide() + binding.progressBar.hide() + + if (adapter.itemCount == 0) { + when (loadState.refresh) { + is LoadState.NotLoading -> { + if (loadState.append is LoadState.NotLoading && loadState.source.refresh is LoadState.NotLoading) { + binding.statusView.show() + binding.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null) } } - } else { - binding.statusView.hide() - } - - binding.progressBar.visible(refreshState == LoadState.Loading && adapter.itemCount == 0) + is LoadState.Error -> { + binding.statusView.show() - if (refreshState is LoadState.NotLoading && !initialRefreshDone) { - // jump to top after the initial refresh finished - binding.recyclerView.scrollToPosition(0) - initialRefreshDone = true - } - - if (refreshState != LoadState.Loading) { - binding.swipeRefreshLayout.isRefreshing = false + if ((loadState.refresh as LoadState.Error).error is IOException) { + binding.statusView.setup(R.drawable.elephant_offline, R.string.error_network, null) + } else { + binding.statusView.setup(R.drawable.elephant_error, R.string.error_generic, null) + } + } + is LoadState.Loading -> { + binding.progressBar.show() + } } } } From e1d07b236a1664a75c86a7162cff87c0711f5f41 Mon Sep 17 00:00:00 2001 From: Conny Duck Date: Wed, 25 May 2022 21:01:04 +0200 Subject: [PATCH 05/12] add db migration --- .../java/com/keylesspalace/tusky/db/AppDatabase.java | 11 ++++++++++- .../main/java/com/keylesspalace/tusky/di/AppModule.kt | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java index c028c88a8b..22237de816 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java +++ b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java @@ -31,7 +31,7 @@ */ @Database(entities = { DraftEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class, TimelineAccountEntity.class, ConversationEntity.class - }, version = 37) + }, version = 38) public abstract class AppDatabase extends RoomDatabase { public abstract AccountDao accountDao(); @@ -561,4 +561,13 @@ public void migrate(@NonNull SupportSQLiteDatabase database) { database.execSQL("ALTER TABLE `ConversationEntity` ADD COLUMN `s_repliesCount` INTEGER NOT NULL DEFAULT 0"); } }; + + public static final Migration MIGRATION_37_38 = new Migration(37, 38) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + // database needs to be cleaned because the ConversationAccountEntity got a new attribute + database.execSQL("DELETE FROM `ConversationEntity`"); + database.execSQL("ALTER TABLE `ConversationEntity` ADD COLUMN `order` INTEGER NOT NULL DEFAULT 0"); + } + }; } diff --git a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt index 08069ae884..b14c539227 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt @@ -64,7 +64,7 @@ class AppModule { AppDatabase.MIGRATION_26_27, AppDatabase.MIGRATION_27_28, AppDatabase.MIGRATION_28_29, AppDatabase.MIGRATION_29_30, AppDatabase.MIGRATION_30_31, AppDatabase.MIGRATION_31_32, AppDatabase.MIGRATION_32_33, AppDatabase.MIGRATION_33_34, AppDatabase.MIGRATION_34_35, - AppDatabase.MIGRATION_35_36, AppDatabase.MIGRATION_36_37, + AppDatabase.MIGRATION_35_36, AppDatabase.MIGRATION_36_37, AppDatabase.MIGRATION_37_38 ) .build() } From e52bd4456c950edced703e15d982741a19e390dc Mon Sep 17 00:00:00 2001 From: Conny Duck Date: Fri, 27 May 2022 16:57:58 +0200 Subject: [PATCH 06/12] make deleting from conversation db suspending --- .../components/conversation/ConversationsRepository.kt | 9 +++------ .../java/com/keylesspalace/tusky/db/ConversationsDao.kt | 2 +- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRepository.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRepository.kt index 12c5eb0bbd..831f233b84 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRepository.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRepository.kt @@ -24,14 +24,11 @@ import javax.inject.Singleton @Singleton class ConversationsRepository @Inject constructor( - val mastodonApi: MastodonApi, + val db: AppDatabase ) { - fun deleteCacheForAccount(accountId: Long) { - Single.fromCallable { - db.conversationDao().deleteForAccount(accountId) - }.subscribeOn(Schedulers.io()) - .subscribe() + suspend fun deleteCacheForAccount(accountId: Long) { + db.conversationDao().deleteForAccount(accountId) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/db/ConversationsDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/ConversationsDao.kt index d650d42da1..001dbbe5e3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/ConversationsDao.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/ConversationsDao.kt @@ -37,5 +37,5 @@ interface ConversationsDao { fun conversationsForAccount(accountId: Long): PagingSource @Query("DELETE FROM ConversationEntity WHERE accountId = :accountId") - fun deleteForAccount(accountId: Long) + suspend fun deleteForAccount(accountId: Long) } From b6c43676ee6b6175f161a550ab56c53c2e55376a Mon Sep 17 00:00:00 2001 From: Conny Duck Date: Fri, 27 May 2022 17:04:50 +0200 Subject: [PATCH 07/12] reorganize code in ConversationsFragment --- .../conversation/ConversationsFragment.kt | 44 +++++++++---------- 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt index fd9035c506..647284fa3f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt @@ -41,11 +41,7 @@ import com.keylesspalace.tusky.fragment.SFragment import com.keylesspalace.tusky.interfaces.ReselectableFragment import com.keylesspalace.tusky.interfaces.StatusActionListener import com.keylesspalace.tusky.settings.PrefKeys -import com.keylesspalace.tusky.util.CardViewMode -import com.keylesspalace.tusky.util.StatusDisplayOptions -import com.keylesspalace.tusky.util.hide -import com.keylesspalace.tusky.util.show -import com.keylesspalace.tusky.util.viewBinding +import com.keylesspalace.tusky.util.* import com.keylesspalace.tusky.viewdata.AttachmentViewData import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest @@ -65,9 +61,6 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res private val binding by viewBinding(FragmentTimelineBinding::bind) private lateinit var adapter: ConversationAdapter - private lateinit var loadStateAdapter: ConversationLoadStateAdapter - - private var layoutManager: LinearLayoutManager? = null override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return inflater.inflate(R.layout.fragment_timeline, container, false) @@ -90,25 +83,11 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res ) adapter = ConversationAdapter(statusDisplayOptions, this) - loadStateAdapter = ConversationLoadStateAdapter(adapter::retry) - - binding.recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL)) - layoutManager = LinearLayoutManager(view.context) - binding.recyclerView.layoutManager = layoutManager - binding.recyclerView.adapter = adapter.withLoadStateFooter(loadStateAdapter) - (binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false - binding.progressBar.hide() - binding.statusView.hide() + setupRecyclerView() initSwipeToRefresh() - viewLifecycleOwner.lifecycleScope.launch { - viewModel.conversationFlow.collectLatest { pagingData -> - adapter.submitData(pagingData) - } - } - adapter.addLoadStateListener { loadState -> if (loadState.refresh != LoadState.Loading && loadState.source.refresh != LoadState.Loading) { binding.swipeRefreshLayout.isRefreshing = false @@ -153,6 +132,12 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res } }) + viewLifecycleOwner.lifecycleScope.launch { + viewModel.conversationFlow.collectLatest { pagingData -> + adapter.submitData(pagingData) + } + } + lifecycleScope.launchWhenResumed { val useAbsoluteTime = preferences.getBoolean(PrefKeys.ABSOLUTE_TIME_VIEW, false) while (!useAbsoluteTime) { @@ -162,6 +147,17 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res } } + private fun setupRecyclerView() { + binding.recyclerView.setHasFixedSize(true) + binding.recyclerView.layoutManager = LinearLayoutManager(context) + + binding.recyclerView.addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL)) + + (binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false + + binding.recyclerView.adapter = adapter.withLoadStateFooter(ConversationLoadStateAdapter(adapter::retry)) + } + private fun initSwipeToRefresh() { binding.swipeRefreshLayout.setOnRefreshListener { adapter.refresh() @@ -275,7 +271,7 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res override fun onReselect() { if (isAdded) { - layoutManager?.scrollToPosition(0) + binding.recyclerView.layoutManager?.scrollToPosition(0) binding.recyclerView.stopScroll() } } From e62b276c2de3606e983e8172968878e4c791a23e Mon Sep 17 00:00:00 2001 From: Conny Duck Date: Fri, 27 May 2022 17:08:19 +0200 Subject: [PATCH 08/12] delete NetworkStateViewHolder --- .../tusky/adapter/NetworkStateViewHolder.kt | 42 ------------------- .../ConversationLoadStateAdapter.kt | 25 ++++++++--- 2 files changed, 19 insertions(+), 48 deletions(-) delete mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/NetworkStateViewHolder.kt diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/NetworkStateViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/NetworkStateViewHolder.kt deleted file mode 100644 index cf75599082..0000000000 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/NetworkStateViewHolder.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* Copyright 2019 Conny Duck - * - * This file is a part of Tusky. - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 3 of the - * License, or (at your option) any later version. - * - * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Tusky; if not, - * see . */ - -package com.keylesspalace.tusky.adapter - -import androidx.paging.LoadState -import androidx.recyclerview.widget.RecyclerView -import com.keylesspalace.tusky.databinding.ItemNetworkStateBinding -import com.keylesspalace.tusky.util.visible - -class NetworkStateViewHolder( - private val binding: ItemNetworkStateBinding, - private val retryCallback: () -> Unit -) : RecyclerView.ViewHolder(binding.root) { - - fun setUpWithNetworkState(state: LoadState) { - binding.progressBar.visible(state == LoadState.Loading) - binding.retryButton.visible(state is LoadState.Error) - val msg = if (state is LoadState.Error) { - state.error.message - } else { - null - } - binding.errorMsg.visible(msg != null) - binding.errorMsg.text = msg - binding.retryButton.setOnClickListener { - retryCallback() - } - } -} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationLoadStateAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationLoadStateAdapter.kt index c7224c4d2e..7ff4daa741 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationLoadStateAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationLoadStateAdapter.kt @@ -19,22 +19,35 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.paging.LoadState import androidx.paging.LoadStateAdapter -import com.keylesspalace.tusky.adapter.NetworkStateViewHolder import com.keylesspalace.tusky.databinding.ItemNetworkStateBinding +import com.keylesspalace.tusky.util.BindingHolder +import com.keylesspalace.tusky.util.visible class ConversationLoadStateAdapter( private val retryCallback: () -> Unit -) : LoadStateAdapter() { +) : LoadStateAdapter>() { - override fun onBindViewHolder(holder: NetworkStateViewHolder, loadState: LoadState) { - holder.setUpWithNetworkState(loadState) + override fun onBindViewHolder(holder: BindingHolder, loadState: LoadState) { + val binding = holder.binding + binding.progressBar.visible(loadState == LoadState.Loading) + binding.retryButton.visible(loadState is LoadState.Error) + val msg = if (loadState is LoadState.Error) { + loadState.error.message + } else { + null + } + binding.errorMsg.visible(msg != null) + binding.errorMsg.text = msg + binding.retryButton.setOnClickListener { + retryCallback() + } } override fun onCreateViewHolder( parent: ViewGroup, loadState: LoadState - ): NetworkStateViewHolder { + ): BindingHolder { val binding = ItemNetworkStateBinding.inflate(LayoutInflater.from(parent.context), parent, false) - return NetworkStateViewHolder(binding, retryCallback) + return BindingHolder(binding) } } From cbc52a6d9f66e2f9483865c86cd8a046c1ef393f Mon Sep 17 00:00:00 2001 From: Conny Duck Date: Fri, 27 May 2022 17:13:39 +0200 Subject: [PATCH 09/12] cleanup imports --- .../tusky/components/conversation/ConversationsFragment.kt | 6 +++++- .../components/conversation/ConversationsRepository.kt | 4 ---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt index 647284fa3f..0667793fe6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt @@ -41,7 +41,11 @@ import com.keylesspalace.tusky.fragment.SFragment import com.keylesspalace.tusky.interfaces.ReselectableFragment import com.keylesspalace.tusky.interfaces.StatusActionListener import com.keylesspalace.tusky.settings.PrefKeys -import com.keylesspalace.tusky.util.* +import com.keylesspalace.tusky.util.CardViewMode +import com.keylesspalace.tusky.util.StatusDisplayOptions +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.viewdata.AttachmentViewData import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRepository.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRepository.kt index 831f233b84..3f074be5b1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRepository.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRepository.kt @@ -16,15 +16,11 @@ package com.keylesspalace.tusky.components.conversation import com.keylesspalace.tusky.db.AppDatabase -import com.keylesspalace.tusky.network.MastodonApi -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.schedulers.Schedulers import javax.inject.Inject import javax.inject.Singleton @Singleton class ConversationsRepository @Inject constructor( - val db: AppDatabase ) { From 28d36a9cffff55280f281ccd7f72e761c57b4da6 Mon Sep 17 00:00:00 2001 From: Conny Duck Date: Fri, 27 May 2022 17:17:26 +0200 Subject: [PATCH 10/12] add 38.json --- .../38.json | 875 ++++++++++++++++++ 1 file changed, 875 insertions(+) create mode 100644 app/schemas/com.keylesspalace.tusky.db.AppDatabase/38.json diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/38.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/38.json new file mode 100644 index 0000000000..391d6b8626 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/38.json @@ -0,0 +1,875 @@ +{ + "formatVersion": 1, + "database": { + "version": 38, + "identityHash": "798fc8d34064eb671c079689d4650ea5", + "entities": [ + { + "tableName": "DraftEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "failedToSend", + "columnName": "failedToSend", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationsSignUps` INTEGER NOT NULL, `notificationsUpdates` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL, `oauthScopes` TEXT NOT NULL, `unifiedPushUrl` TEXT NOT NULL, `pushPubKey` TEXT NOT NULL, `pushPrivKey` TEXT NOT NULL, `pushAuth` TEXT NOT NULL, `pushServerKey` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSignUps", + "columnName": "notificationsSignUps", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsUpdates", + "columnName": "notificationsUpdates", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "oauthScopes", + "columnName": "oauthScopes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unifiedPushUrl", + "columnName": "unifiedPushUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPubKey", + "columnName": "pushPubKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPrivKey", + "columnName": "pushPrivKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushAuth", + "columnName": "pushAuth", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushServerKey", + "columnName": "pushServerKey", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "minPollDuration", + "columnName": "minPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollDuration", + "columnName": "maxPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "charactersReservedPerUrl", + "columnName": "charactersReservedPerUrl", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `repliesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `card` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repliesCount", + "columnName": "repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expanded", + "columnName": "expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentCollapsed", + "columnName": "contentCollapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentShowing", + "columnName": "contentShowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "card", + "columnName": "card", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `order` INTEGER NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_repliesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.repliesCount", + "columnName": "s_repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.tags", + "columnName": "s_tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.muted", + "columnName": "s_muted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '798fc8d34064eb671c079689d4650ea5')" + ] + } +} \ No newline at end of file From 15433ee72ba4467087d35843348028497d3df1f0 Mon Sep 17 00:00:00 2001 From: Conny Duck Date: Fri, 27 May 2022 18:28:42 +0200 Subject: [PATCH 11/12] honor fabHide setting in ConversationsFragment --- .../conversation/ConversationAdapter.kt | 10 +++- .../conversation/ConversationsFragment.kt | 55 +++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationAdapter.kt index 683f16d270..a5a8ed27d2 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationAdapter.kt @@ -25,10 +25,18 @@ import com.keylesspalace.tusky.interfaces.StatusActionListener import com.keylesspalace.tusky.util.StatusDisplayOptions class ConversationAdapter( - private val statusDisplayOptions: StatusDisplayOptions, + private var statusDisplayOptions: StatusDisplayOptions, private val listener: StatusActionListener ) : PagingDataAdapter(CONVERSATION_COMPARATOR) { + var mediaPreviewEnabled: Boolean + get() = statusDisplayOptions.mediaPreviewEnabled + set(mediaPreviewEnabled) { + statusDisplayOptions = statusDisplayOptions.copy( + mediaPreviewEnabled = mediaPreviewEnabled + ) + } + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ConversationViewHolder { val view = LayoutInflater.from(parent.context).inflate(R.layout.item_conversation, parent, false) return ConversationViewHolder(view, statusDisplayOptions, listener) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt index 0667793fe6..b05df2f8b9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt @@ -22,6 +22,7 @@ import android.view.ViewGroup import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.PopupMenu import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.paging.LoadState import androidx.preference.PreferenceManager @@ -30,14 +31,18 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.SimpleItemAnimator import at.connyduck.sparkbutton.helpers.Utils +import autodispose2.androidx.lifecycle.autoDispose import com.keylesspalace.tusky.R import com.keylesspalace.tusky.StatusListActivity import com.keylesspalace.tusky.adapter.StatusBaseViewHolder +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.appstore.PreferenceChangedEvent import com.keylesspalace.tusky.components.account.AccountActivity import com.keylesspalace.tusky.databinding.FragmentTimelineBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.fragment.SFragment +import com.keylesspalace.tusky.interfaces.ActionButtonActivity import com.keylesspalace.tusky.interfaces.ReselectableFragment import com.keylesspalace.tusky.interfaces.StatusActionListener import com.keylesspalace.tusky.settings.PrefKeys @@ -47,6 +52,7 @@ import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.viewdata.AttachmentViewData +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch @@ -60,12 +66,17 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res @Inject lateinit var viewModelFactory: ViewModelFactory + @Inject + lateinit var eventHub: EventHub + private val viewModel: ConversationsViewModel by viewModels { viewModelFactory } private val binding by viewBinding(FragmentTimelineBinding::bind) private lateinit var adapter: ConversationAdapter + private var hideFab = false + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return inflater.inflate(R.layout.fragment_timeline, container, false) } @@ -136,6 +147,24 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res } }) + hideFab = preferences.getBoolean(PrefKeys.FAB_HIDE, false) + binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) { + val composeButton = (activity as ActionButtonActivity).actionButton + if (composeButton != null) { + if (hideFab) { + if (dy > 0 && composeButton.isShown) { + composeButton.hide() // hides the button if we're scrolling down + } else if (dy < 0 && !composeButton.isShown) { + composeButton.show() // shows it if we are scrolling up + } + } else if (!composeButton.isShown) { + composeButton.show() + } + } + } + }) + viewLifecycleOwner.lifecycleScope.launch { viewModel.conversationFlow.collectLatest { pagingData -> adapter.submitData(pagingData) @@ -149,6 +178,15 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res delay(1.toDuration(DurationUnit.MINUTES)) } } + + eventHub.events + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this, Lifecycle.Event.ON_DESTROY) + .subscribe { event -> + if (event is PreferenceChangedEvent) { + onPreferenceChanged(event.preferenceKey) + } + } } private fun setupRecyclerView() { @@ -290,6 +328,23 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res .show() } + private fun onPreferenceChanged(key: String) { + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) + when (key) { + PrefKeys.FAB_HIDE -> { + hideFab = sharedPreferences.getBoolean(PrefKeys.FAB_HIDE, false) + } + PrefKeys.MEDIA_PREVIEW_ENABLED -> { + val enabled = accountManager.activeAccount!!.mediaPreviewEnabled + val oldMediaPreviewEnabled = adapter.mediaPreviewEnabled + if (enabled != oldMediaPreviewEnabled) { + adapter.mediaPreviewEnabled = enabled + adapter.notifyItemRangeChanged(0, adapter.itemCount) + } + } + } + } + companion object { fun newInstance() = ConversationsFragment() } From dd7435e44969d6897d2341aee08a9eb07c5b4c3a Mon Sep 17 00:00:00 2001 From: Conny Duck Date: Mon, 30 May 2022 18:15:12 +0200 Subject: [PATCH 12/12] set page size to 30 --- .../tusky/components/conversation/ConversationsViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt index 61ab8eb4f3..684c6f0118 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt @@ -41,7 +41,7 @@ class ConversationsViewModel @Inject constructor( @OptIn(ExperimentalPagingApi::class) val conversationFlow = Pager( - config = PagingConfig(pageSize = 20), + config = PagingConfig(pageSize = 30), remoteMediator = ConversationsRemoteMediator(accountManager.activeAccount!!.id, api, database), pagingSourceFactory = { database.conversationDao().conversationsForAccount(accountManager.activeAccount!!.id) } )