From 870f02f64ebac954ad7c331efb0956819aabd567 Mon Sep 17 00:00:00 2001 From: Manuel Date: Mon, 7 Apr 2025 09:07:20 +0000 Subject: [PATCH 01/54] Translated using Weblate (Italian) Currently translated at 100.0% (701 of 701 strings) Translated using Weblate (Italian) Currently translated at 100.0% (701 of 701 strings) Co-authored-by: Manuel Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/it/ Translation: Tusky/Tusky --- app/src/main/res/values-it/strings.xml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 12e62d04a..3bdc806aa 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -620,9 +620,9 @@ Account totali Ricarica Hashtag di tendenza - Questa è la tua Timeline principale. Mostra i post più recenti degli account che segui. -\n -\nPer esplorare gli account puoi scoprirli in una delle altre timeline. Per esempio la timeline locale della tua istanza [iconica gmd_group]. O puoi cercarli per fine [iconica gmd_search]; per esempio cerca \"Tusky\" per trovare il nostro account Mastodon. + Questa è la tua Timeline principale. Mostra i post più recenti degli account che segui. +\n +\nPer esplorare gli account puoi scoprirli in una delle altre timeline. Per esempio la timeline locale della tua istanza {{group}}. O puoi cercarli per fine {{search}}; per esempio cerca \"Tusky\" per trovare il nostro account Mastodon. Sconosciuto Immagine Titolo @@ -692,11 +692,11 @@ Vedi filtro Ora segui l\'hashtag #%1$s Hashtag #%1$s silenziato come avvertimento - Questa è la vista delle tue liste. Puoi definire un numero di liste private a cui aggiungere account. -\n -\nRICORDA che puoi aggiungere alle liste solo account che già segui. -\n -\nQueste liste possono essere utilizzate come scheda dalle Impostazioni account [iconics gmd_account_circle] [iconics gmd_navigate_next] Schede. + Questa è la vista delle tue liste. Puoi definire un numero di liste private a cui aggiungere account. +\n +\nRICORDA che puoi aggiungere alle liste solo account che già segui. +\n +\nQueste liste possono essere utilizzate come scheda dalle Impostazioni account {{manage_accounts}} {{chevron_right}} Schede. Post di tendenza Silenziamento di %1$s fallito: %2$s Riattivazione di %1$s fallito: %2$s @@ -788,8 +788,8 @@ Sondaggio %1$s allegato multimediale - %1$s allegati multimediale - %1$s allegati multimediale + %1$s allegati multimediali + %1$s allegati multimediali Rispondi Menzione privata From fc081eaf26219ce42714e33384d3ad5bb5d3dd89 Mon Sep 17 00:00:00 2001 From: Janusz Leidgens Date: Mon, 7 Apr 2025 09:07:20 +0000 Subject: [PATCH 02/54] Translated using Weblate (German) Currently translated at 100.0% (701 of 701 strings) Co-authored-by: Janusz Leidgens Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/de/ Translation: Tusky/Tusky --- app/src/main/res/values-de/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index d9b7525e9..b723c10ff 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -663,7 +663,7 @@ \n \nBEACHTE, dass du nur Profile hinzufügen kannst, denen du folgst. \n -\nDiese Listen können als Tab in den Kontoeinstellungen [iconics gmd_account_circle] [iconics gmd_navigate_next] Tabs festgelegt werden. +\nDiese Listen können als Tab in den Kontoeinstellungen {{manage_accounts}} {{chevron_right}} Tabs festgelegt werden. Hier befinden sich deine privaten Nachrichten – auch bekannt als Unterhaltungen oder »direct messages« (DM, Direktnachricht). \n \nPrivate Nachrichten werden erstellt, indem die Beitragssichtbarkeit {{public}} auf {{mail}} Direkt gesetzt wird und ein oder mehrere Profile erwähnt werden. From e2a0ccb1410c044301562fc903092405948c0844 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Tue, 8 Apr 2025 21:40:51 +0200 Subject: [PATCH 03/54] migrate account list to viewmodel & paging (#5028) This was the last of our fragments that didn't have a ViewModel and still used the old custom pagination Additional benefits: - Way better error handling (the old one didn't even work, in some cases it would look like success when it really failed) - smooter scrolling --- .../LoadStateFooterAdapter.kt} | 4 +- .../accountlist/AccountListActivity.kt | 7 +- .../accountlist/AccountListFragment.kt | 357 ++++-------------- .../accountlist/AccountListPagingSource.kt | 34 ++ .../accountlist/AccountListRemoteMediator.kt | 119 ++++++ .../accountlist/AccountListViewModel.kt | 240 ++++++++++++ .../components/accountlist/AccountViewData.kt | 33 ++ .../accountlist/adapter/AccountAdapter.kt | 117 +----- .../accountlist/adapter/BlocksAdapter.kt | 54 ++- .../accountlist/adapter/FollowAdapter.kt | 20 +- .../adapter/FollowRequestsAdapter.kt | 34 +- .../accountlist/adapter/MutesAdapter.kt | 118 +++--- .../conversation/ConversationsFragment.kt | 3 +- .../tusky/network/MastodonApi.kt | 4 +- .../tusky/view/EndlessOnScrollListener.kt | 48 --- .../main/res/layout/fragment_account_list.xml | 45 ++- app/src/main/res/layout/item_footer.xml | 12 - app/src/main/res/values/strings.xml | 10 + 18 files changed, 654 insertions(+), 605 deletions(-) rename app/src/main/java/com/keylesspalace/tusky/{components/conversation/ConversationLoadStateAdapter.kt => adapter/LoadStateFooterAdapter.kt} (95%) create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/accountlist/AccountListPagingSource.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/accountlist/AccountListRemoteMediator.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/accountlist/AccountListViewModel.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/accountlist/AccountViewData.kt delete mode 100644 app/src/main/java/com/keylesspalace/tusky/view/EndlessOnScrollListener.kt delete mode 100644 app/src/main/res/layout/item_footer.xml diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationLoadStateAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/LoadStateFooterAdapter.kt similarity index 95% rename from app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationLoadStateAdapter.kt rename to app/src/main/java/com/keylesspalace/tusky/adapter/LoadStateFooterAdapter.kt index b8373f986..99f510623 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationLoadStateAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/LoadStateFooterAdapter.kt @@ -13,7 +13,7 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky.components.conversation +package com.keylesspalace.tusky.adapter import android.view.LayoutInflater import android.view.ViewGroup @@ -23,7 +23,7 @@ import com.keylesspalace.tusky.databinding.ItemNetworkStateBinding import com.keylesspalace.tusky.util.BindingHolder import com.keylesspalace.tusky.util.visible -class ConversationLoadStateAdapter( +class LoadStateFooterAdapter( private val retryCallback: () -> Unit ) : LoadStateAdapter>() { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/accountlist/AccountListActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/accountlist/AccountListActivity.kt index b8d972c43..0c556f946 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/accountlist/AccountListActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/accountlist/AccountListActivity.kt @@ -61,8 +61,11 @@ class AccountListActivity : BottomSheetActivity() { setDisplayShowHomeEnabled(true) } - supportFragmentManager.commit { - replace(R.id.fragment_container, AccountListFragment.newInstance(type, id)) + if (supportFragmentManager.findFragmentById(R.id.fragment_container) == null) { + supportFragmentManager.commit { + val fragment = AccountListFragment.newInstance(type, id) + replace(R.id.fragment_container, fragment) + } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/accountlist/AccountListFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/accountlist/AccountListFragment.kt index e43b87d45..ffc7ab4d6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/accountlist/AccountListFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/accountlist/AccountListFragment.kt @@ -20,21 +20,22 @@ import android.os.Bundle import android.util.Log import android.view.View import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope +import androidx.paging.LoadState import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.SimpleItemAnimator -import at.connyduck.calladapter.networkresult.fold +import com.google.android.material.snackbar.BaseTransientBottomBar import com.google.android.material.snackbar.Snackbar import com.keylesspalace.tusky.BottomSheetActivity import com.keylesspalace.tusky.PostLookupFallbackBehavior import com.keylesspalace.tusky.R import com.keylesspalace.tusky.StatusListActivity +import com.keylesspalace.tusky.adapter.LoadStateFooterAdapter import com.keylesspalace.tusky.components.account.AccountActivity import com.keylesspalace.tusky.components.accountlist.AccountListActivity.Type -import com.keylesspalace.tusky.components.accountlist.adapter.AccountAdapter import com.keylesspalace.tusky.components.accountlist.adapter.BlocksAdapter import com.keylesspalace.tusky.components.accountlist.adapter.FollowAdapter import com.keylesspalace.tusky.components.accountlist.adapter.FollowRequestsAdapter @@ -42,25 +43,21 @@ import com.keylesspalace.tusky.components.accountlist.adapter.FollowRequestsHead import com.keylesspalace.tusky.components.accountlist.adapter.MutesAdapter import com.keylesspalace.tusky.databinding.FragmentAccountListBinding import com.keylesspalace.tusky.db.AccountManager -import com.keylesspalace.tusky.entity.Relationship -import com.keylesspalace.tusky.entity.TimelineAccount import com.keylesspalace.tusky.interfaces.AccountActionListener import com.keylesspalace.tusky.interfaces.LinkListener -import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.settings.PrefKeys -import com.keylesspalace.tusky.util.HttpHeaderLink import com.keylesspalace.tusky.util.ensureBottomPadding import com.keylesspalace.tusky.util.getSerializableCompat import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation import com.keylesspalace.tusky.util.viewBinding -import com.keylesspalace.tusky.view.EndlessOnScrollListener +import com.keylesspalace.tusky.util.visible import dagger.hilt.android.AndroidEntryPoint +import dagger.hilt.android.lifecycle.withCreationCallback import javax.inject.Inject -import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -import retrofit2.Response @AndroidEntryPoint class AccountListFragment : @@ -68,9 +65,6 @@ class AccountListFragment : AccountActionListener, LinkListener { - @Inject - lateinit var api: MastodonApi - @Inject lateinit var accountManager: AccountManager @@ -79,13 +73,20 @@ class AccountListFragment : private val binding by viewBinding(FragmentAccountListBinding::bind) + private val viewModel: AccountListViewModel by viewModels( + extrasProducer = { + defaultViewModelCreationExtras.withCreationCallback { factory -> + factory.create( + type = requireArguments().getSerializableCompat(ARG_TYPE)!!, + accountId = requireArguments().getString(ARG_ID) + ) + } + } + ) + private lateinit var type: Type private var id: String? = null - private var adapter: AccountAdapter<*>? = null - private var fetching = false - private var bottomId: String? = null - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) type = requireArguments().getSerializableCompat(ARG_TYPE)!! @@ -123,31 +124,59 @@ class AccountListFragment : } else -> FollowAdapter(this, animateAvatar, animateEmojis, showBotOverlay) } - this.adapter = adapter - if (binding.recyclerView.adapter == null) { - binding.recyclerView.adapter = adapter + + binding.recyclerView.adapter = adapter.withLoadStateFooter(LoadStateFooterAdapter(adapter::retry)) + + binding.swipeRefreshLayout.setOnRefreshListener { adapter.refresh() } + + lifecycleScope.launch { + viewModel.accountPager.collectLatest { pagingData -> + adapter.submitData(pagingData) + } } - val scrollListener = object : EndlessOnScrollListener(layoutManager) { - override fun onLoadMore(totalItemsCount: Int, view: RecyclerView) { - if (bottomId == null) { - return + lifecycleScope.launch { + viewModel.uiEvents.collect { event -> + val message = if (event.throwable != null) { + getString(event.message, event.user, event.throwable.message ?: getString(R.string.error_generic)) + } else { + getString(event.message, event.user) } - fetchAccounts(adapter, bottomId) + Snackbar.make(binding.recyclerView, message, Snackbar.LENGTH_LONG) + .setAction(event.actionText, event.action) + .addCallback(object : BaseTransientBottomBar.BaseCallback() { + override fun onDismissed(transientBottomBar: Snackbar, eventType: Int) { + viewModel.consumeEvent(event) + } + }) + .show() } } - binding.recyclerView.addOnScrollListener(scrollListener) - - binding.swipeRefreshLayout.setOnRefreshListener { fetchAccounts(adapter) } + adapter.addLoadStateListener { loadState -> + binding.progressBar.visible( + loadState.refresh == LoadState.Loading && adapter.itemCount == 0 + ) - fetchAccounts(adapter) - } + if (loadState.refresh != LoadState.Loading) { + binding.swipeRefreshLayout.isRefreshing = false + } - override fun onDestroyView() { - // Clear the adapter to prevent leaking the View - adapter = null - super.onDestroyView() + if (loadState.refresh is LoadState.Error) { + binding.recyclerView.hide() + binding.messageView.show() + val errorState = loadState.refresh as LoadState.Error + binding.messageView.setup(errorState.error) { adapter.retry() } + Log.w(TAG, "error loading accounts", errorState.error) + } else if (loadState.refresh is LoadState.NotLoading && adapter.itemCount == 0) { + binding.recyclerView.hide() + binding.messageView.show() + binding.messageView.setup(R.drawable.elephant_friend_empty, R.string.message_empty) + } else { + binding.recyclerView.show() + binding.messageView.hide() + } + } } override fun onViewTag(tag: String) { @@ -165,275 +194,29 @@ class AccountListFragment : } override fun onMute(mute: Boolean, id: String, position: Int, notifications: Boolean) { - viewLifecycleOwner.lifecycleScope.launch { - try { - if (!mute) { - api.unmuteAccount(id) - } else { - api.muteAccount(id, notifications) - } - onMuteSuccess(mute, id, position, notifications) - } catch (_: Throwable) { - onMuteFailure(mute, id, notifications) - } - } - } - - private fun onMuteSuccess(muted: Boolean, id: String, position: Int, notifications: Boolean) { - val mutesAdapter = adapter as MutesAdapter - if (muted) { - mutesAdapter.updateMutingNotifications(id, notifications, position) - return - } - val unmutedUser = mutesAdapter.removeItem(position) - - if (unmutedUser != null) { - Snackbar.make(binding.recyclerView, R.string.confirmation_unmuted, Snackbar.LENGTH_LONG) - .setAction(R.string.action_undo) { - mutesAdapter.addItem(unmutedUser, position) - onMute(true, id, position, notifications) - } - .show() - } - } - - private fun onMuteFailure(mute: Boolean, accountId: String, notifications: Boolean) { - val verb = if (mute) { - if (notifications) { - "mute (notifications = true)" - } else { - "mute (notifications = false)" - } + if (mute) { + viewModel.mute(id, notifications) } else { - "unmute" + viewModel.unmute(id) } - Log.e(TAG, "Failed to $verb account id $accountId") } override fun onBlock(block: Boolean, id: String, position: Int) { - viewLifecycleOwner.lifecycleScope.launch { - try { - if (!block) { - api.unblockAccount(id) - } else { - api.blockAccount(id) - } - onBlockSuccess(block, id, position) - } catch (_: Throwable) { - onBlockFailure(block, id) - } - } - } - - private fun onBlockSuccess(blocked: Boolean, id: String, position: Int) { - if (blocked) { - return - } - val blocksAdapter = adapter as BlocksAdapter - val unblockedUser = blocksAdapter.removeItem(position) - - if (unblockedUser != null) { - Snackbar.make( - binding.recyclerView, - R.string.confirmation_unblocked, - Snackbar.LENGTH_LONG - ) - .setAction(R.string.action_undo) { - blocksAdapter.addItem(unblockedUser, position) - onBlock(true, id, position) - } - .show() - } - } - - private fun onBlockFailure(block: Boolean, accountId: String) { - val verb = if (block) { - "block" - } else { - "unblock" - } - Log.e(TAG, "Failed to $verb account accountId $accountId") + viewModel.unblock(id) } override fun onRespondToFollowRequest(accept: Boolean, id: String, position: Int) { - viewLifecycleOwner.lifecycleScope.launch { - if (accept) { - api.authorizeFollowRequest(id) - } else { - api.rejectFollowRequest(id) - }.fold( - onSuccess = { - onRespondToFollowRequestSuccess(position) - }, - onFailure = { throwable -> - val verb = if (accept) { - "accept" - } else { - "reject" - } - Log.e(TAG, "Failed to $verb account id $id.", throwable) - } - ) - } - } - - private fun onRespondToFollowRequestSuccess(position: Int) { - val followRequestsAdapter = adapter as FollowRequestsAdapter - followRequestsAdapter.removeItem(position) - } - - private suspend fun getFetchCallByListType(fromId: String?): Response> { - return when (type) { - Type.FOLLOWS -> { - val accountId = requireId(type, id) - api.accountFollowing(accountId, fromId) - } - Type.FOLLOWERS -> { - val accountId = requireId(type, id) - api.accountFollowers(accountId, fromId) - } - Type.BLOCKS -> api.blocks(fromId) - Type.MUTES -> api.mutes(fromId) - Type.FOLLOW_REQUESTS -> api.followRequests(fromId) - Type.REBLOGGED -> { - val statusId = requireId(type, id) - api.statusRebloggedBy(statusId, fromId) - } - Type.FAVOURITED -> { - val statusId = requireId(type, id) - api.statusFavouritedBy(statusId, fromId) - } - } - } - - private fun requireId(type: Type, id: String?): String { - return requireNotNull(id) { "id must not be null for type " + type.name } - } - - private fun fetchAccounts(adapter: AccountAdapter<*>, fromId: String? = null) { - if (fetching) { - return - } - fetching = true - binding.swipeRefreshLayout.isRefreshing = true - - if (fromId != null) { - binding.recyclerView.post { adapter.setBottomLoading(true) } - } - - viewLifecycleOwner.lifecycleScope.launch { - try { - val response = getFetchCallByListType(fromId) - - if (!response.isSuccessful) { - onFetchAccountsFailure(adapter, Exception(response.message())) - return@launch - } - - val accountList = response.body() - - if (accountList == null) { - onFetchAccountsFailure(adapter, Exception(response.message())) - return@launch - } - - val linkHeader = response.headers()["Link"] - onFetchAccountsSuccess(adapter, accountList, linkHeader) - } catch (exception: Exception) { - if (exception is CancellationException) { - // Scope is cancelled, probably because the fragment is destroyed. - // We must not touch any views anymore, so rethrow the exception. - // (CancellationException in a cancelled scope is normal and will be ignored) - throw exception - } - onFetchAccountsFailure(adapter, exception) - } - } - } - - private fun onFetchAccountsSuccess( - adapter: AccountAdapter<*>, - accounts: List, - linkHeader: String? - ) { - adapter.setBottomLoading(false) - binding.swipeRefreshLayout.isRefreshing = false - - val links = HttpHeaderLink.parse(linkHeader) - val next = HttpHeaderLink.findByRelationType(links, "next") - val fromId = next?.uri?.getQueryParameter("max_id") - - if (adapter.itemCount > 0) { - adapter.addItems(accounts) - } else { - adapter.update(accounts) - } - - if (adapter is MutesAdapter) { - fetchRelationships(adapter, accounts.map { it.id }) - } - - bottomId = fromId - - fetching = false - - if (adapter.itemCount == 0) { - binding.messageView.show() - binding.messageView.setup( - R.drawable.elephant_friend_empty, - R.string.message_empty, - null - ) - } else { - binding.messageView.hide() - } - } - - private fun fetchRelationships(mutesAdapter: MutesAdapter, ids: List) { - viewLifecycleOwner.lifecycleScope.launch { - api.relationships(ids) - .fold( - onSuccess = { relationships -> - onFetchRelationshipsSuccess(mutesAdapter, relationships) - }, - onFailure = { throwable -> - Log.e(TAG, "Fetch failure for relationships of accounts: $ids", throwable) - } - ) - } - } - - private fun onFetchRelationshipsSuccess( - mutesAdapter: MutesAdapter, - relationships: List - ) { - val mutingNotificationsMap = HashMap() - relationships.map { mutingNotificationsMap.put(it.id, it.mutingNotifications) } - mutesAdapter.updateMutingNotificationsMap(mutingNotificationsMap) - } - - private fun onFetchAccountsFailure(adapter: AccountAdapter<*>, throwable: Throwable) { - fetching = false - binding.swipeRefreshLayout.isRefreshing = false - Log.e(TAG, "Fetch failure", throwable) - - if (adapter.itemCount == 0) { - binding.messageView.show() - binding.messageView.setup(throwable) { - binding.messageView.hide() - this.fetchAccounts(adapter, null) - } - } + viewModel.respondToFollowRequest(accept, id) } companion object { - private const val TAG = "AccountList" // logging tag + private const val TAG = "AccountListFragment" private const val ARG_TYPE = "type" private const val ARG_ID = "id" fun newInstance(type: Type, id: String? = null): AccountListFragment { return AccountListFragment().apply { - arguments = Bundle(3).apply { + arguments = Bundle(2).apply { putSerializable(ARG_TYPE, type) putString(ARG_ID, id) } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/accountlist/AccountListPagingSource.kt b/app/src/main/java/com/keylesspalace/tusky/components/accountlist/AccountListPagingSource.kt new file mode 100644 index 000000000..be5ee6909 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/accountlist/AccountListPagingSource.kt @@ -0,0 +1,34 @@ +/* Copyright 2025 Tusky Contributors. + * + * 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.components.accountlist + +import androidx.paging.PagingSource +import androidx.paging.PagingState + +class AccountListPagingSource( + private val accounts: List, + private val nextKey: String? +) : PagingSource() { + override fun getRefreshKey(state: PagingState): String? = null + + override suspend fun load(params: LoadParams): LoadResult { + return if (params is LoadParams.Refresh) { + LoadResult.Page(accounts, null, nextKey) + } else { + LoadResult.Page(emptyList(), null, null) + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/accountlist/AccountListRemoteMediator.kt b/app/src/main/java/com/keylesspalace/tusky/components/accountlist/AccountListRemoteMediator.kt new file mode 100644 index 000000000..62686219a --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/accountlist/AccountListRemoteMediator.kt @@ -0,0 +1,119 @@ +/* Copyright 2025 Tusky Contributors. + * + * 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.components.accountlist + +import androidx.paging.ExperimentalPagingApi +import androidx.paging.LoadType +import androidx.paging.PagingState +import androidx.paging.RemoteMediator +import at.connyduck.calladapter.networkresult.getOrElse +import com.keylesspalace.tusky.components.accountlist.AccountListActivity.Type +import com.keylesspalace.tusky.entity.TimelineAccount +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.HttpHeaderLink +import retrofit2.HttpException +import retrofit2.Response + +@OptIn(ExperimentalPagingApi::class) +class AccountListRemoteMediator( + private val api: MastodonApi, + private val viewModel: AccountListViewModel, + private val fetchRelationships: Boolean +) : RemoteMediator() { + + override suspend fun load( + loadType: LoadType, + state: PagingState + ): MediatorResult { + return try { + val response = request(loadType) + ?: return MediatorResult.Success(endOfPaginationReached = true) + + return applyResponse(response) + } catch (e: Exception) { + MediatorResult.Error(e) + } + } + + private suspend fun request(loadType: LoadType): Response>? { + return when (loadType) { + LoadType.PREPEND -> null + LoadType.APPEND -> getFetchCallByListType(fromId = viewModel.nextKey) + LoadType.REFRESH -> { + viewModel.nextKey = null + viewModel.accounts.clear() + getFetchCallByListType(null) + } + } + } + + private suspend fun applyResponse(response: Response>): MediatorResult { + val accounts = response.body() + if (!response.isSuccessful || accounts == null) { + return MediatorResult.Error(HttpException(response)) + } + + val links = HttpHeaderLink.parse(response.headers()["Link"]) + viewModel.nextKey = HttpHeaderLink.findByRelationType(links, "next")?.uri?.getQueryParameter("max_id") + + val relationships = if (fetchRelationships) { + api.relationships(accounts.map { it.id }).getOrElse { e -> + return MediatorResult.Error(e) + } + } else { + emptyList() + } + + val viewModels = accounts.map { account -> + account.toViewData( + mutingNotifications = relationships.find { it.id == account.id }?.mutingNotifications == true + ) + } + + viewModel.accounts.addAll(viewModels) + viewModel.invalidate() + + return MediatorResult.Success(endOfPaginationReached = viewModel.nextKey == null) + } + + private fun requireId(type: Type, id: String?): String { + return requireNotNull(id) { "id must not be null for type " + type.name } + } + + private suspend fun getFetchCallByListType(fromId: String?): Response> { + return when (viewModel.type) { + Type.FOLLOWS -> { + val accountId = requireId(viewModel.type, viewModel.accountId) + api.accountFollowing(accountId, fromId) + } + Type.FOLLOWERS -> { + val accountId = requireId(viewModel.type, viewModel.accountId) + api.accountFollowers(accountId, fromId) + } + Type.BLOCKS -> api.blocks(fromId) + Type.MUTES -> api.mutes(fromId) + Type.FOLLOW_REQUESTS -> api.followRequests(fromId) + Type.REBLOGGED -> { + val statusId = requireId(viewModel.type, viewModel.accountId) + api.statusRebloggedBy(statusId, fromId) + } + Type.FAVOURITED -> { + val statusId = requireId(viewModel.type, viewModel.accountId) + api.statusFavouritedBy(statusId, fromId) + } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/accountlist/AccountListViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/accountlist/AccountListViewModel.kt new file mode 100644 index 000000000..b07bf7420 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/accountlist/AccountListViewModel.kt @@ -0,0 +1,240 @@ +/* Copyright 2025 Tusky Contributors. + * + * 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.components.accountlist + +import android.view.View +import androidx.annotation.StringRes +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.ExperimentalPagingApi +import androidx.paging.InvalidatingPagingSourceFactory +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.cachedIn +import at.connyduck.calladapter.networkresult.fold +import at.connyduck.calladapter.networkresult.onFailure +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.network.MastodonApi +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +@HiltViewModel(assistedFactory = AccountListViewModel.Factory::class) +class AccountListViewModel @AssistedInject constructor( + private val api: MastodonApi, + @Assisted("type") val type: AccountListActivity.Type, + @Assisted("id") val accountId: String? +) : ViewModel() { + + private val factory = InvalidatingPagingSourceFactory { + AccountListPagingSource(accounts.toList(), nextKey) + } + + @OptIn(ExperimentalPagingApi::class) + val accountPager = Pager( + config = PagingConfig(40), + remoteMediator = AccountListRemoteMediator(api, this, fetchRelationships = type == AccountListActivity.Type.MUTES), + pagingSourceFactory = factory + ).flow + .cachedIn(viewModelScope) + + val accounts: MutableList = mutableListOf() + var nextKey: String? = null + + private val _uiEvents = MutableStateFlow>(emptyList()) + val uiEvents: Flow = _uiEvents.map { it.firstOrNull() }.filterNotNull().distinctUntilChanged() + + fun invalidate() { + factory.invalidate() + } + + // this is called by the mute notification toggle + fun mute(accountId: String, notifications: Boolean) { + val accountViewData = accounts.find { it.id == accountId } ?: return + viewModelScope.launch { + api.muteAccount(accountId, notifications).onFailure { e -> + sendEvent( + SnackbarEvent( + message = R.string.mute_failure, + user = "@${accountViewData.account.username}", + throwable = e, + actionText = R.string.action_retry, + action = { mute(accountId, notifications) } + ) + ) + } + } + } + + // this is called when unmuting is undone + private fun remute(accountViewData: AccountViewData) { + viewModelScope.launch { + api.muteAccount(accountViewData.id).fold({ + accounts.add(accountViewData) + invalidate() + }, { e -> + sendEvent( + SnackbarEvent( + message = R.string.mute_failure, + user = "@${accountViewData.account.username}", + throwable = e, + actionText = R.string.action_retry, + action = { block(accountViewData) } + ) + ) + }) + } + } + + fun unmute(accountId: String) { + val accountViewData = accounts.find { it.id == accountId } ?: return + viewModelScope.launch { + api.unmuteAccount(accountId).fold({ + accounts.removeIf { it.id == accountId } + invalidate() + sendEvent( + SnackbarEvent( + message = R.string.unmute_success, + user = "@${accountViewData.account.username}", + throwable = null, + actionText = R.string.action_undo, + action = { remute(accountViewData) } + ) + ) + }, { error -> + sendEvent( + SnackbarEvent( + message = R.string.unmute_failure, + user = "@${accountViewData.account.username}", + throwable = error, + actionText = R.string.action_retry, + action = { unmute(accountId) } + ) + ) + }) + } + } + + fun unblock(accountId: String) { + val accountViewData = accounts.find { it.id == accountId } ?: return + viewModelScope.launch { + api.unblockAccount(accountId).fold({ + accounts.removeIf { it.id == accountId } + invalidate() + sendEvent( + SnackbarEvent( + message = R.string.unblock_success, + user = "@${accountViewData.account.username}", + throwable = null, + actionText = R.string.action_undo, + action = { block(accountViewData) } + ) + ) + }, { e -> + sendEvent( + SnackbarEvent( + message = R.string.unblock_failure, + user = "@${accountViewData.account.username}", + throwable = e, + actionText = R.string.action_retry, + action = { unblock(accountId) } + ) + ) + }) + } + } + + private fun block(accountViewData: AccountViewData) { + viewModelScope.launch { + api.blockAccount(accountViewData.id).fold({ + accounts.add(accountViewData) + invalidate() + }, { e -> + sendEvent( + SnackbarEvent( + message = R.string.block_failure, + user = "@${accountViewData.account.username}", + throwable = e, + actionText = R.string.action_retry, + action = { block(accountViewData) } + ) + ) + }) + } + } + + fun respondToFollowRequest(accept: Boolean, accountId: String) { + val accountViewData = accounts.find { it.id == accountId } ?: return + viewModelScope.launch { + if (accept) { + api.authorizeFollowRequest(accountId) + } else { + api.rejectFollowRequest(accountId) + }.fold({ + accounts.removeIf { it.id == accountId } + invalidate() + }, { e -> + sendEvent( + SnackbarEvent( + message = if (accept) R.string.accept_follow_request_failure else R.string.reject_follow_request_failure, + user = "@${accountViewData.account.username}", + throwable = e, + actionText = R.string.action_retry, + action = { respondToFollowRequest(accept, accountId) } + ) + ) + }) + } + } + + fun consumeEvent(event: SnackbarEvent) { + println("event consumed $event") + _uiEvents.update { uiEvents -> + uiEvents - event + } + } + + private fun sendEvent(event: SnackbarEvent) { + println("event sent $event") + _uiEvents.update { uiEvents -> + uiEvents + event + } + } + + @AssistedFactory + interface Factory { + fun create( + @Assisted("type") type: AccountListActivity.Type, + @Assisted("id") accountId: String? + ): AccountListViewModel + } +} + +class SnackbarEvent( + @StringRes val message: Int, + val user: String, + @StringRes val actionText: Int, + val action: (View) -> Unit, + val throwable: Throwable? = null +) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/accountlist/AccountViewData.kt b/app/src/main/java/com/keylesspalace/tusky/components/accountlist/AccountViewData.kt new file mode 100644 index 000000000..fc9ffd265 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/accountlist/AccountViewData.kt @@ -0,0 +1,33 @@ +/* Copyright 2025 Tusky Contributors. + * + * 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.components.accountlist + +import com.keylesspalace.tusky.entity.TimelineAccount + +data class AccountViewData( + val account: TimelineAccount, + val mutingNotifications: Boolean +) { + val id: String + get() = account.id +} + +fun TimelineAccount.toViewData( + mutingNotifications: Boolean +) = AccountViewData( + account = this, + mutingNotifications = mutingNotifications +) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/AccountAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/AccountAdapter.kt index ac327ac03..7c00aa3ac 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/AccountAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/AccountAdapter.kt @@ -14,111 +14,34 @@ * see . */ package com.keylesspalace.tusky.components.accountlist.adapter -import android.view.LayoutInflater -import android.view.ViewGroup +import androidx.paging.PagingDataAdapter +import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView -import com.keylesspalace.tusky.databinding.ItemFooterBinding -import com.keylesspalace.tusky.entity.TimelineAccount +import com.keylesspalace.tusky.components.accountlist.AccountViewData import com.keylesspalace.tusky.interfaces.AccountActionListener -import com.keylesspalace.tusky.util.BindingHolder -import com.keylesspalace.tusky.util.removeDuplicatesTo -/** Generic adapter with bottom loading indicator. */ -abstract class AccountAdapter internal constructor( +abstract class AccountAdapter( protected val accountActionListener: AccountActionListener, protected val animateAvatar: Boolean, protected val animateEmojis: Boolean, protected val showBotOverlay: Boolean -) : RecyclerView.Adapter() { - - protected var accountList: MutableList = mutableListOf() - private var bottomLoading: Boolean = false - - override fun getItemCount(): Int { - return accountList.size + if (bottomLoading) 1 else 0 - } - - abstract fun createAccountViewHolder(parent: ViewGroup): AVH - - abstract fun onBindAccountViewHolder(viewHolder: AVH, position: Int) - - final override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) { - @Suppress("UNCHECKED_CAST") - this.onBindAccountViewHolder(holder as AVH, position) - } - } - - final override fun onCreateViewHolder( - parent: ViewGroup, - viewType: Int - ): RecyclerView.ViewHolder { - return when (viewType) { - VIEW_TYPE_ACCOUNT -> this.createAccountViewHolder(parent) - VIEW_TYPE_FOOTER -> this.createFooterViewHolder(parent) - else -> error("Unknown item type: $viewType") - } - } - - private fun createFooterViewHolder(parent: ViewGroup): RecyclerView.ViewHolder { - val binding = ItemFooterBinding.inflate(LayoutInflater.from(parent.context), parent, false) - return BindingHolder(binding) - } - - override fun getItemViewType(position: Int): Int { - return if (position == accountList.size && bottomLoading) { - VIEW_TYPE_FOOTER - } else { - VIEW_TYPE_ACCOUNT - } - } - - fun update(newAccounts: List) { - accountList = newAccounts.removeDuplicatesTo(ArrayList()) - notifyDataSetChanged() - } - - fun addItems(newAccounts: List) { - val end = accountList.size - val last = accountList[end - 1] - if (newAccounts.none { it.id == last.id }) { - accountList.addAll(newAccounts) - notifyItemRangeInserted(end, newAccounts.size) - } - } - - fun setBottomLoading(loading: Boolean) { - val wasLoading = bottomLoading - if (wasLoading == loading) { - return - } - bottomLoading = loading - if (loading) { - notifyItemInserted(accountList.size) - } else { - notifyItemRemoved(accountList.size) - } - } - - fun removeItem(position: Int): TimelineAccount? { - if (position < 0 || position >= accountList.size) { - return null - } - val account = accountList.removeAt(position) - notifyItemRemoved(position) - return account - } - - fun addItem(account: TimelineAccount, position: Int) { - if (position < 0 || position > accountList.size) { - return - } - accountList.add(position, account) - notifyItemInserted(position) - } +) : PagingDataAdapter(AccountViewDataDifferCallback) { companion object { - const val VIEW_TYPE_ACCOUNT = 0 - const val VIEW_TYPE_FOOTER = 1 + private val AccountViewDataDifferCallback = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: AccountViewData, + newItem: AccountViewData + ): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame( + oldItem: AccountViewData, + newItem: AccountViewData + ): Boolean { + return oldItem == newItem + } + } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/BlocksAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/BlocksAdapter.kt index c1132e7f7..2df515d74 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/BlocksAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/BlocksAdapter.kt @@ -38,42 +38,38 @@ class BlocksAdapter( showBotOverlay = showBotOverlay ) { - override fun createAccountViewHolder(parent: ViewGroup): BindingHolder { - val binding = ItemBlockedUserBinding.inflate( - LayoutInflater.from(parent.context), - parent, - false + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder { + return BindingHolder( + ItemBlockedUserBinding.inflate(LayoutInflater.from(parent.context), parent, false) ) - return BindingHolder(binding) } - override fun onBindAccountViewHolder( - viewHolder: BindingHolder, - position: Int - ) { - val account = accountList[position] - val binding = viewHolder.binding - val context = binding.root.context + override fun onBindViewHolder(viewHolder: BindingHolder, position: Int) { + getItem(position)?.let { viewData -> + val account = viewData.account + val binding = viewHolder.binding + val context = binding.root.context - val emojifiedName = account.name.emojify( - account.emojis, - binding.blockedUserDisplayName, - animateEmojis - ) - binding.blockedUserDisplayName.text = emojifiedName - val formattedUsername = context.getString(R.string.post_username_format, account.username) - binding.blockedUserUsername.text = formattedUsername + val emojifiedName = account.name.emojify( + account.emojis, + binding.blockedUserDisplayName, + animateEmojis + ) + binding.blockedUserDisplayName.text = emojifiedName + val formattedUsername = context.getString(R.string.post_username_format, account.username) + binding.blockedUserUsername.text = formattedUsername - val avatarRadius = context.resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp) - loadAvatar(account.avatar, binding.blockedUserAvatar, avatarRadius, animateAvatar) + val avatarRadius = context.resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp) + loadAvatar(account.avatar, binding.blockedUserAvatar, avatarRadius, animateAvatar) - binding.blockedUserBotBadge.visible(showBotOverlay && account.bot) + binding.blockedUserBotBadge.visible(showBotOverlay && account.bot) - binding.blockedUserUnblock.setOnClickListener { - accountActionListener.onBlock(false, account.id, position) - } - binding.root.setOnClickListener { - accountActionListener.onViewAccount(account.id) + binding.blockedUserUnblock.setOnClickListener { + accountActionListener.onBlock(false, account.id, position) + } + binding.root.setOnClickListener { + accountActionListener.onViewAccount(account.id) + } } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/FollowAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/FollowAdapter.kt index 87b62486d..b91f09f12 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/FollowAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/FollowAdapter.kt @@ -34,18 +34,20 @@ class FollowAdapter( showBotOverlay = showBotOverlay ) { - override fun createAccountViewHolder(parent: ViewGroup): AccountViewHolder { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AccountViewHolder { val binding = ItemAccountBinding.inflate(LayoutInflater.from(parent.context), parent, false) return AccountViewHolder(binding) } - override fun onBindAccountViewHolder(viewHolder: AccountViewHolder, position: Int) { - viewHolder.setupWithAccount( - accountList[position], - animateAvatar, - animateEmojis, - showBotOverlay - ) - viewHolder.setupActionListener(accountActionListener) + override fun onBindViewHolder(viewHolder: AccountViewHolder, position: Int) { + getItem(position)?.let { viewData -> + viewHolder.setupWithAccount( + viewData.account, + animateAvatar, + animateEmojis, + showBotOverlay + ) + viewHolder.setupActionListener(accountActionListener) + } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/FollowRequestsAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/FollowRequestsAdapter.kt index fc860e59e..7487fb6f4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/FollowRequestsAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/FollowRequestsAdapter.kt @@ -35,28 +35,20 @@ class FollowRequestsAdapter( animateEmojis = animateEmojis, showBotOverlay = showBotOverlay ) { - - override fun createAccountViewHolder(parent: ViewGroup): FollowRequestViewHolder { - val binding = ItemFollowRequestBinding.inflate( - LayoutInflater.from(parent.context), - parent, - false - ) - return FollowRequestViewHolder( - binding, - accountActionListener, - linkListener, - showHeader = false - ) + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FollowRequestViewHolder { + val binding = ItemFollowRequestBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return FollowRequestViewHolder(binding, accountActionListener, linkListener, showHeader = false) } - override fun onBindAccountViewHolder(viewHolder: FollowRequestViewHolder, position: Int) { - viewHolder.setupWithAccount( - account = accountList[position], - animateAvatar = animateAvatar, - animateEmojis = animateEmojis, - showBotOverlay = showBotOverlay - ) - viewHolder.setupActionListener(accountActionListener, accountList[position].id) + override fun onBindViewHolder(viewHolder: FollowRequestViewHolder, position: Int) { + getItem(position)?.let { viewData -> + viewHolder.setupWithAccount( + account = viewData.account, + animateAvatar = animateAvatar, + animateEmojis = animateEmojis, + showBotOverlay = showBotOverlay + ) + viewHolder.setupActionListener(accountActionListener, viewData.account.id) + } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/MutesAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/MutesAdapter.kt index d685730de..7e87e7d3b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/MutesAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/MutesAdapter.kt @@ -39,82 +39,58 @@ class MutesAdapter( showBotOverlay = showBotOverlay ) { - private val mutingNotificationsMap = HashMap() - - override fun createAccountViewHolder(parent: ViewGroup): BindingHolder { - val binding = ItemMutedUserBinding.inflate( - LayoutInflater.from(parent.context), - parent, - false + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder { + return BindingHolder( + ItemMutedUserBinding.inflate(LayoutInflater.from(parent.context), parent, false) ) - return BindingHolder(binding) } - override fun onBindAccountViewHolder( - viewHolder: BindingHolder, - position: Int - ) { - val account = accountList[position] - val binding = viewHolder.binding - val context = binding.root.context - - val mutingNotifications = mutingNotificationsMap[account.id] - - val emojifiedName = account.name.emojify( - account.emojis, - binding.mutedUserDisplayName, - animateEmojis - ) - binding.mutedUserDisplayName.text = emojifiedName - - val formattedUsername = context.getString(R.string.post_username_format, account.username) - binding.mutedUserUsername.text = formattedUsername - - val avatarRadius = context.resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp) - loadAvatar(account.avatar, binding.mutedUserAvatar, avatarRadius, animateAvatar) - - binding.mutedUserBotBadge.visible(showBotOverlay && account.bot) - - val unmuteString = context.getString(R.string.action_unmute_desc, formattedUsername) - binding.mutedUserUnmute.contentDescription = unmuteString - ViewCompat.setTooltipText(binding.mutedUserUnmute, unmuteString) - - binding.mutedUserMuteNotifications.setOnCheckedChangeListener(null) + override fun onBindViewHolder(viewHolder: BindingHolder, position: Int) { + getItem(position)?.let { viewData -> + val account = viewData.account + val binding = viewHolder.binding + val context = binding.root.context - binding.mutedUserMuteNotifications.isChecked = if (mutingNotifications == null) { - binding.mutedUserMuteNotifications.isEnabled = false - true - } else { - binding.mutedUserMuteNotifications.isEnabled = true - mutingNotifications - } - - binding.mutedUserUnmute.setOnClickListener { - accountActionListener.onMute( - false, - account.id, - viewHolder.bindingAdapterPosition, - false - ) - } - binding.mutedUserMuteNotifications.setOnCheckedChangeListener { _, isChecked -> - accountActionListener.onMute( - true, - account.id, - viewHolder.bindingAdapterPosition, - isChecked + val emojifiedName = account.name.emojify( + account.emojis, + binding.mutedUserDisplayName, + animateEmojis ) + binding.mutedUserDisplayName.text = emojifiedName + + val formattedUsername = context.getString(R.string.post_username_format, account.username) + binding.mutedUserUsername.text = formattedUsername + + val avatarRadius = context.resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp) + loadAvatar(account.avatar, binding.mutedUserAvatar, avatarRadius, animateAvatar) + + binding.mutedUserBotBadge.visible(showBotOverlay && account.bot) + + val unmuteString = context.getString(R.string.action_unmute_desc, formattedUsername) + binding.mutedUserUnmute.contentDescription = unmuteString + ViewCompat.setTooltipText(binding.mutedUserUnmute, unmuteString) + + binding.mutedUserMuteNotifications.setOnCheckedChangeListener(null) + + binding.mutedUserMuteNotifications.isChecked = viewData.mutingNotifications + + binding.mutedUserUnmute.setOnClickListener { + accountActionListener.onMute( + false, + account.id, + viewHolder.bindingAdapterPosition, + false + ) + } + binding.mutedUserMuteNotifications.setOnCheckedChangeListener { _, isChecked -> + accountActionListener.onMute( + true, + account.id, + viewHolder.bindingAdapterPosition, + isChecked + ) + } + binding.root.setOnClickListener { accountActionListener.onViewAccount(account.id) } } - binding.root.setOnClickListener { accountActionListener.onViewAccount(account.id) } - } - - fun updateMutingNotifications(id: String, mutingNotifications: Boolean, position: Int) { - mutingNotificationsMap[id] = mutingNotifications - notifyItemChanged(position) - } - - fun updateMutingNotificationsMap(newMutingNotificationsMap: HashMap) { - mutingNotificationsMap.putAll(newMutingNotificationsMap) - notifyDataSetChanged() } } 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 4303c700f..60eb749bb 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 @@ -35,6 +35,7 @@ import at.connyduck.sparkbutton.helpers.Utils import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.keylesspalace.tusky.R import com.keylesspalace.tusky.StatusListActivity +import com.keylesspalace.tusky.adapter.LoadStateFooterAdapter import com.keylesspalace.tusky.appstore.ConversationsLoadingEvent import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.PreferenceChangedEvent @@ -224,7 +225,7 @@ class ConversationsFragment : (binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false binding.recyclerView.adapter = - adapter.withLoadStateFooter(ConversationLoadStateAdapter(adapter::retry)) + adapter.withLoadStateFooter(LoadStateFooterAdapter(adapter::retry)) } private fun refreshContent() { 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 40617328b..c329ea0fe 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -402,10 +402,10 @@ interface MastodonApi { suspend fun unsubscribeAccount(@Path("id") accountId: String): NetworkResult @GET("api/v1/blocks") - suspend fun blocks(@Query("max_id") maxId: String?): Response> + suspend fun blocks(@Query("max_id") maxId: String? = null): Response> @GET("api/v1/mutes") - suspend fun mutes(@Query("max_id") maxId: String?): Response> + suspend fun mutes(@Query("max_id") maxId: String? = null): Response> @GET("api/v1/domain_blocks") suspend fun domainBlocks( diff --git a/app/src/main/java/com/keylesspalace/tusky/view/EndlessOnScrollListener.kt b/app/src/main/java/com/keylesspalace/tusky/view/EndlessOnScrollListener.kt deleted file mode 100644 index c240adf90..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/view/EndlessOnScrollListener.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* Copyright 2017 Andrew Dawson - * - * 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.view - -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView - -abstract class EndlessOnScrollListener(private val layoutManager: LinearLayoutManager) : - RecyclerView.OnScrollListener() { - private var previousTotalItemCount = 0 - - override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) { - val totalItemCount = layoutManager.itemCount - val lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition() - - if (totalItemCount < previousTotalItemCount) { - previousTotalItemCount = totalItemCount - } - if (totalItemCount != previousTotalItemCount) { - previousTotalItemCount = totalItemCount - } - if (lastVisibleItemPosition + VISIBLE_THRESHOLD > totalItemCount) { - onLoadMore(totalItemCount, view) - } - } - - fun reset() { - previousTotalItemCount = 0 - } - - abstract fun onLoadMore(totalItemsCount: Int, view: RecyclerView) - - companion object { - private const val VISIBLE_THRESHOLD = 15 - } -} diff --git a/app/src/main/res/layout/fragment_account_list.xml b/app/src/main/res/layout/fragment_account_list.xml index b4f9df5be..a2c287ccc 100644 --- a/app/src/main/res/layout/fragment_account_list.xml +++ b/app/src/main/res/layout/fragment_account_list.xml @@ -1,34 +1,31 @@ - - - + android:layout_height="match_parent" + android:clipToPadding="false" + android:paddingBottom="@dimen/recyclerview_bottom_padding_no_actionbutton" /> - + - - - - - - + + + diff --git a/app/src/main/res/layout/item_footer.xml b/app/src/main/res/layout/item_footer.xml deleted file mode 100644 index aed4880bb..000000000 --- a/app/src/main/res/layout/item_footer.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d904fb85e..d8deded4d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -916,4 +916,14 @@ Yes No Discard caption changes? + + Unblocked %1$s + Unmuted %1$s + Failed blocking %1$s: %2$s + Failed muting %1$s: %2$s + Failed unblocking %1$s: %2$s + Failed unmuting %1$s: %2$s + Failed accepting follow request from %1$s: %2$s + Failed rejecting follow request from %1$s: %2$s + From e65de1bea9edf565562d39f0d444ca7ee724037e Mon Sep 17 00:00:00 2001 From: Janusz Leidgens Date: Tue, 8 Apr 2025 21:46:06 +0200 Subject: [PATCH 04/54] Fix unused translations lint warnings (#5036) Fixes #5032 I added all languages that where marked as unused to the locale file. The only language that maybe is not really worth putting in there is berber with 36 translations right now. But in the end those 36 may be better (if they are correct). --- app/lint-baseline.xml | 141 +++------------------ app/src/main/res/values/donottranslate.xml | 22 +++- app/src/main/res/xml/locales_config.xml | 10 ++ 3 files changed, 52 insertions(+), 121 deletions(-) diff --git a/app/lint-baseline.xml b/app/lint-baseline.xml index b4d6aa015..f061426ec 100644 --- a/app/lint-baseline.xml +++ b/app/lint-baseline.xml @@ -1,5 +1,5 @@ - + @@ -53,14 +53,14 @@ + message="Overriding `@layout/exo_player_control_view` which is marked as private in androidx.media3:media3-ui:1.6.0. If deliberate, use tools:override="true", otherwise pick a different name."> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -501,7 +402,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -677,7 +578,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index c8ff12146..5365079e5 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -39,6 +39,7 @@ @string/system_default + Bahasa Indonesia Català Čeština Cymraeg @@ -49,11 +50,14 @@ Español Euskara Français + Frysk Gaeilge Gàidhlig Galego íslenska Italiano + latviešu valoda + Lëtzebuergesch Magyar Nederlands Norsk @@ -61,7 +65,9 @@ Polski Português (Brasil) Português (Portugal) - Slovenščina + slovenski jezik + slovenský jazyk + Suomi Svenska Taqbaylit Tiếng Việt @@ -87,10 +93,15 @@ 中文(简体) 中文(香港) 日本語 + ⵜⴰⵎⴰⵣⵉⵖⵜ + Ελληνικά + മലയാളം + සිංහල default + in ca cs cy @@ -101,11 +112,14 @@ es eu fr + fy ga gd gl is it + lv + lb hu nl nb-NO @@ -114,6 +128,8 @@ pt-BR pt-PT sl + sk + fi sv kab vi @@ -139,6 +155,10 @@ zh-CN zh-HK ja + ber + el + ml + si diff --git a/app/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml index a409a913c..ce8603ab0 100644 --- a/app/src/main/res/xml/locales_config.xml +++ b/app/src/main/res/xml/locales_config.xml @@ -49,4 +49,14 @@ + + + + + + + + + + From b2b9e7e8f77591680c52d8a2c2d9bbbb5bc0f211 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Tue, 8 Apr 2025 21:46:18 +0200 Subject: [PATCH 05/54] fix AccountMediaFragment insets (#5041) see https://github.com/tuskyapp/Tusky/issues/5002#issuecomment-2781122748 --- .../tusky/components/account/media/AccountMediaFragment.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaFragment.kt index 083c93fc9..a031a18fb 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaFragment.kt @@ -35,8 +35,10 @@ import com.keylesspalace.tusky.ViewMediaActivity import com.keylesspalace.tusky.databinding.FragmentTimelineBinding import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.entity.Attachment +import com.keylesspalace.tusky.interfaces.ActionButtonActivity import com.keylesspalace.tusky.interfaces.RefreshableFragment import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.util.ensureBottomPadding import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.openLink import com.keylesspalace.tusky.util.show @@ -78,6 +80,9 @@ class AccountMediaFragment : val useBlurhash = preferences.getBoolean(PrefKeys.USE_BLURHASH, true) + val hasFab = (activity as? ActionButtonActivity?)?.actionButton != null + binding.recyclerView.ensureBottomPadding(fab = hasFab) + val adapter = AccountMediaGridAdapter( useBlurhash = useBlurhash, context = view.context, From cce76aabd5c26bd968cf143f49577fa88d8403fc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 8 Apr 2025 21:46:28 +0200 Subject: [PATCH 06/54] Update Kotlin to v1.10.2 (#5043) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [org.jetbrains.kotlinx:kotlinx-coroutines-test](https://redirect.github.com/Kotlin/kotlinx.coroutines) | `1.10.1` -> `1.10.2` | [![age](https://developer.mend.io/api/mc/badges/age/maven/org.jetbrains.kotlinx:kotlinx-coroutines-test/1.10.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/org.jetbrains.kotlinx:kotlinx-coroutines-test/1.10.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/org.jetbrains.kotlinx:kotlinx-coroutines-test/1.10.1/1.10.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/org.jetbrains.kotlinx:kotlinx-coroutines-test/1.10.1/1.10.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [org.jetbrains.kotlinx:kotlinx-coroutines-android](https://redirect.github.com/Kotlin/kotlinx.coroutines) | `1.10.1` -> `1.10.2` | [![age](https://developer.mend.io/api/mc/badges/age/maven/org.jetbrains.kotlinx:kotlinx-coroutines-android/1.10.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/org.jetbrains.kotlinx:kotlinx-coroutines-android/1.10.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/org.jetbrains.kotlinx:kotlinx-coroutines-android/1.10.1/1.10.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/org.jetbrains.kotlinx:kotlinx-coroutines-android/1.10.1/1.10.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
Kotlin/kotlinx.coroutines (org.jetbrains.kotlinx:kotlinx-coroutines-test) ### [`v1.10.2`](https://redirect.github.com/Kotlin/kotlinx.coroutines/blob/HEAD/CHANGES.md#Version-1102) [Compare Source](https://redirect.github.com/Kotlin/kotlinx.coroutines/compare/1.10.1...1.10.2) - Fixed the `kotlinx-coroutines-debug` JAR file including the `module-info.class` file twice, resulting in failures in various tooling ([#​4314](https://redirect.github.com/Kotlin/kotlinx.coroutines/issues/4314)). Thanks, [@​RyuNen344](https://redirect.github.com/RyuNen344)! - Fixed `Flow.stateIn` hanging when the scope is cancelled in advance or the flow is empty ([#​4322](https://redirect.github.com/Kotlin/kotlinx.coroutines/issues/4322)). Thanks, [@​francescotescari](https://redirect.github.com/francescotescari)! - Improved handling of dispatcher failures in `.limitedParallelism` ([#​4330](https://redirect.github.com/Kotlin/kotlinx.coroutines/issues/4330)) and during flow collection ([#​4272](https://redirect.github.com/Kotlin/kotlinx.coroutines/issues/4272)). - Fixed `runBlocking` failing to run its coroutine to completion in some cases if its JVM thread got interrupted ([#​4399](https://redirect.github.com/Kotlin/kotlinx.coroutines/issues/4399)). - Small tweaks, fixes, and documentation improvements.
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about these updates again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/tuskyapp/Tusky). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- gradle/verification-metadata.xml | 45 ++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fa720fa34..18f4d9652 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -25,7 +25,7 @@ androidx-work = "2.10.0" androidx-room = "2.6.1" bouncycastle = "1.70" conscrypt = "2.5.3" -coroutines = "1.10.1" +coroutines = "1.10.2" diffx = "1.1.1" emoji2 = "1.4.0" filemoji-compat = "3.2.7" diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 84d145e6f..66bceab0d 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -18099,6 +18099,14 @@ + + + + + + + + @@ -18136,6 +18144,11 @@ + + + + + @@ -18174,6 +18187,14 @@ + + + + + + + + @@ -18221,6 +18242,14 @@ + + + + + + + + @@ -18285,6 +18314,14 @@ + + + + + + + + @@ -18309,6 +18346,14 @@ + + + + + + + + From 751667c412a305a39234aa3e12e72de427799646 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Wed, 9 Apr 2025 22:09:16 +0200 Subject: [PATCH 07/54] fix text selection shrinking the view in ViewThreadFragment (#5034) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://layer8.space/@andre/114268763071943595 wtf is this bug 😳 Happens only on Android 15 I have no idea what exactly is going on here, I found this fix by trial and error. `outsideOverlay` seems to look exactly the same as `outsideInset` so i hope this is safe. --- app/src/main/res/layout-sw640dp/fragment_view_thread.xml | 2 +- app/src/main/res/layout/fragment_view_thread.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/layout-sw640dp/fragment_view_thread.xml b/app/src/main/res/layout-sw640dp/fragment_view_thread.xml index 0ef8377f5..894a3c051 100644 --- a/app/src/main/res/layout-sw640dp/fragment_view_thread.xml +++ b/app/src/main/res/layout-sw640dp/fragment_view_thread.xml @@ -26,7 +26,7 @@ android:background="?android:attr/colorBackground" android:clipToPadding="false" android:paddingBottom="@dimen/recyclerview_bottom_padding_no_actionbutton" - android:scrollbarStyle="outsideInset" + android:scrollbarStyle="outsideOverlay" android:scrollbars="vertical" /> Date: Fri, 11 Apr 2025 05:33:53 +0000 Subject: [PATCH 08/54] Translated using Weblate (Persian) Currently translated at 100.0% (709 of 709 strings) Co-authored-by: Danial Behzadi Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/fa/ Translation: Tusky/Tusky --- app/src/main/res/values-fa/strings.xml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index d257f7182..a8f518aeb 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -782,4 +782,12 @@ بله دور انداختن تغییرات عنوان؟ نه - + خموشی %1$s رفع شد + خموش کردن %1$s شکست خورد: %2$s + رفع کردن مسدودی %1$s شکست خورد: %2$s + رفع کردن خموشی %1$s شکست خورد: %2$s + رد کردن درخواست پی‌گیری %1$s شکست خورد: %2$s + انسداد %1$s رفع شد + مسدود کردن %1$s شکست خورد: %2$s + پذیرفتن درخواست پی‌گیری %1$s شکست خورد: %2$s + \ No newline at end of file From 743a25db850f3eda7e665fb777dcb17e0de9cfc0 Mon Sep 17 00:00:00 2001 From: Eric Date: Fri, 11 Apr 2025 05:33:53 +0000 Subject: [PATCH 09/54] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (709 of 709 strings) Co-authored-by: Eric Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/zh_Hans/ Translation: Tusky/Tusky --- app/src/main/res/values-zh-rCN/strings.xml | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 644618c20..520e2d6d5 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -666,10 +666,10 @@ 查看筛选器 正在关注话题标签 #%1$s 已不再关注话题标签 #%1$s - 这是你的 列表视图。你可以定义私人列表并添加联系人到列表中。 -\n -\n请注意,你只能将你已关注的账户添加到列表中。 -\n + 这是你的 列表视图。你可以定义私人列表并添加联系人到列表中。 +\n +\n请注意,你只能将你已关注的账户添加到列表中。 +\n \n这些列表可被用作“账户选项{管理账户}} {{chevron_right}}”选项卡中的标签页。 未能静音 %1$s: %2$s 未能取消对 %1$s: %2$s的静音 @@ -774,4 +774,12 @@ 放弃字幕更改? + 封禁 %1$s 失败:%2$s + 静音 %1$s 失败:%2$s + 解封 %1$s 失败:%2$s + 未能解除对 %1$s 的静音:%2$s + 拒绝 %1$s 的关注请求失败了:%2$s + 已解封 %1$s + 已解除对 %1$s 的静音操作 + 接受 %1$s 的关注请求失败了:%2$s \ No newline at end of file From e4725514fd0101e1da1001da8caf006adfd7238f Mon Sep 17 00:00:00 2001 From: Manuel Date: Fri, 11 Apr 2025 05:33:53 +0000 Subject: [PATCH 10/54] Translated using Weblate (Italian) Currently translated at 100.0% (709 of 709 strings) Co-authored-by: Manuel Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/it/ Translation: Tusky/Tusky --- app/src/main/res/values-it/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 3bdc806aa..730b511bc 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -802,4 +802,12 @@ Mostra risultati Scartare le modifiche alla didascalia? + Silenziamento di %1$s fallito: %2$s + Sblocco di %1$s fallito: %2$s + Riattivazione di %1$s fallita: %2$s + Respingimento richiesta follow di %1$s fallita: %2$s + Sbloccato %1$s + Riattivato %1$s + Blocco di %1$s fallito: %2$s + Accettazione del follow di %1$s fallita: %2$s \ No newline at end of file From bdb8d7451c138d73789f4538ba1e62bd7b2b42b2 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Fri, 11 Apr 2025 05:33:53 +0000 Subject: [PATCH 11/54] Translated using Weblate (Ukrainian) Currently translated at 96.8% (687 of 709 strings) Co-authored-by: Ihor Hordiichuk Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/uk/ Translation: Tusky/Tusky --- app/src/main/res/values-uk/strings.xml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index d1a0c939b..1287b20a1 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -681,17 +681,17 @@ Сховати з домашньої стрічки Тут розміщено ваші приватні повідомлення; іноді їх називають розмовами або прямими повідомленнями (DM). \n -\nПриватні повідомлення створюються шляхом налаштування видимості {{public}} допису на {{mail}} Особисту і згадування в тексті одного або кількох користувачів. +\nПриватні повідомлення створюються шляхом налаштування видимості {{public}} допису на {{mail}} Особистий і згадування в тексті одного або кількох користувачів. \n \nНаприклад, ви можете почати з перегляду профілю облікового запису, натиснути кнопку створення [iconics gmd_edit] і змінити видимість. Не вдалося відтворити: %1$s Видалити фільтр \'%1$s\'\? Видалити Хочете зберегти зміни у своєму профілі\? - Це ваше подання списків. Ви можете створити кілька приватних списків і додати до них облікові записи. -\n -\nЗАУВАЖТЕ, що ви можете додавати до своїх списків лише ті облікові записи, за якими слідкуєте. -\n + Це ваше подання списків. Ви можете створити кілька приватних списків і додати до них облікові записи. +\n +\nЗАУВАЖТЕ, що ви можете додавати до своїх списків лише ті облікові записи, за якими слідкуєте. +\n \nЦі списки можна використовувати як вкладку у налаштуваннях Вкладок облікового запису [iconics gmd_account_circle] [iconics gmd_navigate_next]. Сховати хештег #%1$s як попередження Показувати хештег #%1$s @@ -780,4 +780,7 @@ Відповідь Приватна згадка Приватна відповідь + Поширити без сповіщення інших + Показати результати + Адміністратор з %1$s обмежив %2$s, що означає, що ви більше не можете отримувати від них оновлення або взаємодіяти з ними. \ No newline at end of file From 1a7f17b4d868dfe9676905627b6e09b4cf20a2ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=E1=BB=93=20Nh=E1=BA=A5t=20Duy?= Date: Fri, 11 Apr 2025 05:33:54 +0000 Subject: [PATCH 12/54] Translated using Weblate (Vietnamese) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (709 of 709 strings) Co-authored-by: Hồ Nhất Duy Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/vi/ Translation: Tusky/Tusky --- app/src/main/res/values-vi/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 51a8c9fe0..992b7c7e7 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -760,4 +760,12 @@ Bỏ thay đổi chú thích? Không + Đã bỏ ẩn %1$s + Chưa thể chặn %1$s: %2$s + Chưa thể ẩn %1$s: %2$s + Chưa thể bỏ chặn %1$s: %2$s + Chưa thể bỏ ẩn %1$s: %2$s + Chấp nhận yêu cầu theo dõi thất bại từ %1$s: %2$s + Từ chối yêu cầu theo dõi thất bại từ %1$s: %2$s + Đã bỏ chặn %1$s \ No newline at end of file From 509025623e3ad402b8b44a422b7692d039cd6f84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20M?= Date: Fri, 11 Apr 2025 05:33:54 +0000 Subject: [PATCH 13/54] Translated using Weblate (Galician) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (709 of 709 strings) Co-authored-by: José M Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/gl/ Translation: Tusky/Tusky --- app/src/main/res/values-gl/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 0ceaa97b4..61f33d4b8 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -775,4 +775,12 @@ Non Desbotar cambios na descrición? Si + Reactivouse a %1$s + Fallou o bloqueo de %1$s: %2$s + Fallou o silenciamento de %1$s: %2$s + Fallou o desbloqueo de %1$s: %2$s + Fallou a aceptación da solicitude de seguimento de %1$s: %2$s + Desbloqueouse a %1$s + Fallou a reactivación de %1$s: %2$s + Fallou o rexeitamento da solicitude de seguimento de %1$s: %2$s \ No newline at end of file From 3b634b7373b6e5dbec13a558761e1d7b7062c9d7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 11 Apr 2025 08:00:08 +0200 Subject: [PATCH 14/54] Update androidx.room to v2.7.0 (#5048) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [androidx.room:room-testing](https://developer.android.com/jetpack/androidx/releases/room#2.7.0) ([source](https://cs.android.com/androidx/platform/frameworks/support)) | `2.6.1` -> `2.7.0` | [![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.room:room-testing/2.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.room:room-testing/2.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.room:room-testing/2.6.1/2.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.room:room-testing/2.6.1/2.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [androidx.room:room-ktx](https://developer.android.com/jetpack/androidx/releases/room#2.7.0) ([source](https://cs.android.com/androidx/platform/frameworks/support)) | `2.6.1` -> `2.7.0` | [![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.room:room-ktx/2.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.room:room-ktx/2.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.room:room-ktx/2.6.1/2.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.room:room-ktx/2.6.1/2.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [androidx.room:room-paging](https://developer.android.com/jetpack/androidx/releases/room#2.7.0) ([source](https://cs.android.com/androidx/platform/frameworks/support)) | `2.6.1` -> `2.7.0` | [![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.room:room-paging/2.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.room:room-paging/2.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.room:room-paging/2.6.1/2.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.room:room-paging/2.6.1/2.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [androidx.room:room-compiler](https://developer.android.com/jetpack/androidx/releases/room#2.7.0) ([source](https://cs.android.com/androidx/platform/frameworks/support)) | `2.6.1` -> `2.7.0` | [![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.room:room-compiler/2.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.room:room-compiler/2.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.room:room-compiler/2.6.1/2.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.room:room-compiler/2.6.1/2.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about these updates again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/tuskyapp/Tusky). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- gradle/verification-metadata.xml | 235 +++++++++++++++++++++++++++++++ 2 files changed, 236 insertions(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 18f4d9652..8c3f30688 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,7 +22,7 @@ androidx-swiperefresh-layout = "1.1.0" androidx-testing = "2.2.0" androidx-viewpager2 = "1.1.0" androidx-work = "2.10.0" -androidx-room = "2.6.1" +androidx-room = "2.7.0" bouncycastle = "1.70" conscrypt = "2.5.3" coroutines = "1.10.2" diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 66bceab0d..4af2c9847 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -181,6 +181,14 @@ + + + + + + + + @@ -250,6 +258,14 @@ + + + + + + + + @@ -356,6 +372,9 @@ + + + @@ -3122,6 +3141,22 @@ + + + + + + + + + + + + + + + + @@ -3130,6 +3165,14 @@ + + + + + + + + @@ -3138,6 +3181,22 @@ + + + + + + + + + + + + + + + + @@ -3149,6 +3208,14 @@ + + + + + + + + @@ -3157,6 +3224,22 @@ + + + + + + + + + + + + + + + + @@ -3168,6 +3251,22 @@ + + + + + + + + + + + + + + + + @@ -3181,6 +3280,22 @@ + + + + + + + + + + + + + + + + @@ -3192,6 +3307,22 @@ + + + + + + + + + + + + + + + + @@ -3243,6 +3374,22 @@ + + + + + + + + + + + + + + + + @@ -3256,6 +3403,22 @@ + + + + + + + + + + + + + + + + @@ -14424,6 +14587,11 @@ + + + + + @@ -14432,6 +14600,22 @@ + + + + + + + + + + + + + + + + @@ -16873,6 +17057,14 @@ + + + + + + + + @@ -17090,6 +17282,14 @@ + + + + + + + + @@ -17704,6 +17904,14 @@ + + + + + + + + @@ -17776,6 +17984,14 @@ + + + + + + + + @@ -18062,6 +18278,11 @@ + + + + + @@ -18392,6 +18613,9 @@ + + + @@ -18431,6 +18655,9 @@ + + + @@ -18461,6 +18688,14 @@ + + + + + + + + From 80e8a40e8ca0c758ac8b0cb1bcd38e9f4acb865a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 11 Apr 2025 08:00:26 +0200 Subject: [PATCH 15/54] Update dependency com.squareup.okio:okio to v3.11.0 (#5050) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [com.squareup.okio:okio](https://redirect.github.com/square/okio) | `3.10.2` -> `3.11.0` | [![age](https://developer.mend.io/api/mc/badges/age/maven/com.squareup.okio:okio/3.11.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.squareup.okio:okio/3.11.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.squareup.okio:okio/3.10.2/3.11.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.squareup.okio:okio/3.10.2/3.11.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
square/okio (com.squareup.okio:okio) ### [`v3.11.0`](https://redirect.github.com/square/okio/blob/HEAD/CHANGELOG.md#Version-3110) *2025-04-09* - Fix: Clear the deflater's byte array reference - New: Faster implementation of `String.decodeHex()` on Kotlin/JS. - New: Declare `EXACTLY_ONCE` execution for blocks like `Closeable.use {}` and `FileSystem.read {}`. - Upgrade: \[Kotlin 2.1.20]\[kotlin\_2\_1\_20].
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/tuskyapp/Tusky). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- gradle/verification-metadata.xml | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8c3f30688..6f9b5128b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -41,7 +41,7 @@ mockito-kotlin = "5.4.0" moshi = "1.15.2" networkresult-calladapter = "1.2.0" okhttp = "4.12.0" -okio = "3.10.2" +okio = "3.11.0" retrofit = "2.11.0" robolectric = "4.14.1" sparkbutton = "4.2.0" diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 4af2c9847..44cd253cc 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -14713,6 +14713,14 @@
+ + + + + + + + @@ -14750,6 +14758,14 @@ + + + + + + + + From f2e52eecdb3754e8864133a9211901460541eb38 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 11 Apr 2025 08:01:34 +0200 Subject: [PATCH 16/54] Update plugin com.gradle.develocity to v4 (#5045) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | com.gradle.develocity | `3.19.2` -> `4.0` | [![age](https://developer.mend.io/api/mc/badges/age/maven/com.gradle.develocity:com.gradle.develocity.gradle.plugin/4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.gradle.develocity:com.gradle.develocity.gradle.plugin/4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.gradle.develocity:com.gradle.develocity.gradle.plugin/3.19.2/4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.gradle.develocity:com.gradle.develocity.gradle.plugin/3.19.2/4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/tuskyapp/Tusky). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/verification-metadata.xml | 13 +++++++++++++ settings.gradle | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 44cd253cc..6939f0abb 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -14233,6 +14233,14 @@ + + + + + + + + @@ -14278,6 +14286,11 @@ + + + + + diff --git a/settings.gradle b/settings.gradle index 2f0bfca72..422643785 100644 --- a/settings.gradle +++ b/settings.gradle @@ -11,7 +11,7 @@ pluginManagement { } plugins { - id 'com.gradle.develocity' version '3.19.2' + id 'com.gradle.develocity' version '4.0' } def isCI = providers.environmentVariable("CI").present From e2f907a551ad022348df80b0d224c1ff38c09aa2 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Fri, 11 Apr 2025 09:42:44 +0200 Subject: [PATCH 17/54] fix toolbar overlap in FiltersActivity on Android 15 (#5053) closes #5052 --- app/src/main/res/layout/activity_filters.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/layout/activity_filters.xml b/app/src/main/res/layout/activity_filters.xml index 096473090..fa6f08372 100644 --- a/app/src/main/res/layout/activity_filters.xml +++ b/app/src/main/res/layout/activity_filters.xml @@ -14,7 +14,7 @@ android:id="@+id/swipeRefreshLayout" android:layout_width="match_parent" android:layout_height="match_parent" - android:layout_marginTop="?attr/actionBarSize"> + app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"> + android:layout_gravity="center" + android:indeterminate="true" /> Date: Fri, 11 Apr 2025 09:42:58 +0200 Subject: [PATCH 18/54] Update dependency androidx.core:core-ktx to v1.16.0 (#5049) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [androidx.core:core-ktx](https://developer.android.com/jetpack/androidx/releases/core#1.16.0) ([source](https://cs.android.com/androidx/platform/frameworks/support)) | `1.15.0` -> `1.16.0` | [![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.core:core-ktx/1.16.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.core:core-ktx/1.16.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.core:core-ktx/1.15.0/1.16.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.core:core-ktx/1.15.0/1.16.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/tuskyapp/Tusky). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- gradle/verification-metadata.xml | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6f9b5128b..f310eac4b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,7 @@ androidx-appcompat = "1.7.0" androidx-browser = "1.8.0" androidx-cardview = "1.0.0" androidx-constraintlayout = "2.2.1" -androidx-core = "1.15.0" +androidx-core = "1.16.0" androidx-drawerlayout = "1.2.0" androidx-exifinterface = "1.4.0" androidx-fragment = "1.8.6" diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 6939f0abb..17dc9e350 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -541,6 +541,14 @@ + + + + + + + + @@ -560,6 +568,14 @@ + + + + + + + + From dedd3d234b5c69e878c27c6c1c5bdee42e52bdc9 Mon Sep 17 00:00:00 2001 From: ButterflyOfFire Date: Sun, 13 Apr 2025 13:07:14 +0000 Subject: [PATCH 19/54] Translated using Weblate (Kabyle) Currently translated at 53.3% (378 of 709 strings) Co-authored-by: ButterflyOfFire Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/kab/ Translation: Tusky/Tusky --- app/src/main/res/values-kab/strings.xml | 129 +++++++++++++++++++++--- 1 file changed, 114 insertions(+), 15 deletions(-) diff --git a/app/src/main/res/values-kab/strings.xml b/app/src/main/res/values-kab/strings.xml index 7e1631c33..010b3deff 100644 --- a/app/src/main/res/values-kab/strings.xml +++ b/app/src/main/res/values-kab/strings.xml @@ -3,7 +3,7 @@ Qqen ɣer Maṣṭudun Ismenyifen Irewwayen - Ffeɣ + Senser tuqqna Iɣewwaṛen Iɣewwaṛen n umiḍan Ẓreg amaɣnu @@ -14,13 +14,13 @@ Izen-ik·im aṭas i ɣezzif! Agejdan Iccaren - Sensla + Asqerdec Iznan S tririyin Ẓreg amaɣnu-ik \@%1$s - Zeṛ ugar - Zeṛ kra kan + Wali ugar + Wali kra kan Ulac walu da. %1$s yeṭṭafar-ik·ikem-id Err @@ -69,7 +69,7 @@ Anta tummant\? d-acu i gellan d amaynut\? Nadi… - Tiririn… + Tiririt… Tugna n umaɣnu Sider Kkes tijewwiqt-a\? @@ -103,7 +103,7 @@ Tiɣula yettwaffren Isuturen n teḍfeṛt Taɣwalt - Sfeḍ + Kkes Imiḍanen yettwasgugmen Imiḍan yettusḥebsen Tiɣula yettwaffren @@ -124,7 +124,7 @@ Rnu amsizdeg Ẓreg amsizdeg Snulfu-d tabdart - Snifel isem n tabdart + Snifel isem n tebdart Kkes tabdart-a Rnu yiwen umiḍan ɣer tabdart Kkes amiḍan seg tabdart @@ -155,7 +155,7 @@ %1$dis aya %1$dus aya Tettaraḍ-as i @%1$s - awid ugar + Awi-d ugar Idewenniyen Rgel amiḍan Yettnadi… @@ -232,7 +232,7 @@ %1$d n tasinin id yugran Agbur amḥulfu - Creḍ allal n teywalt amzun d amḥulfu + Creḍ amidya amzun d amḥulfu (yettwasɣiwes akked uqeddac) Wennez tikkelt-nniḍen Asali ur yeddi ara. Tuccḍa deg tuzna n tijewwiqt. @@ -242,14 +242,14 @@ %1$s yebḍa izen-ik·im %1$s yerna izen-ik·im ɣer ismenyafen-is Tiririt taruradt - Bḍu - Kkes beṭu - Ffer beṭuyat - Sken-d beṭuyat + Zuzer + Kkes beṭṭu + Ffer izuzar + Sken-d izuzar Ur sgugum ara Ddeg Ihacṭagen - Sken-d beṭuyat + Sken-d izuzar Ihacṭagen Aseqdac nni ur yettwasgugem ara tura %1$s ur yettwaffer ara @@ -268,10 +268,109 @@ Ulac ɣur-k·m ula d yiwet n tjewwiqt yettwasɣawsen. Tijewwiqin yettwasɣawsen Tijewwiqin yettwasɣawsen - Sken-d beṭuyat + Sken-d izuzar Ihacṭagen Isuturen n teḍfeṛt Izuzar Agbur Immed + Tugna + + %1$s uzuzar + %1$s n izuzar + + Suqel + D asawen + Akken yella unagraw + 90 n wussan + Seggelmes amidya + Sewḥel @%1$s? + 14 n wussan + Ur yesɛi ara aglam + Zgel + 30 n wussan + 60 n wussan + 180 n wussan + 365 n wussan + Ḍfeṛ + Da seg ass n %1$s + Ameslaw + Izuzar n imeḍfaṛen kan + Azuzer azayaz + Sken-d izuzar-ik·im + Kkes asqerdec + Sgugem asqerced-a + Ldi umuɣ + Agbur amḥulfu + Wali ameskar n uzuzer + Mdel + Talqayt + Yettwanɣel isem n useqdac + Agbur amḥulfu + Sgugem @%1$s? + Ffer ilɣa + Ubrik + Ameẓyan + Alemmas + Srid + Anedbal + Tisuffaɣ timaynutin + %1$s (%2$s) + Yezzuzer-it + Tabzimt + Azayez + War tabdart + Srid + Tettwaẓreg + Yessuqul… + Iɣefisefka n umaɣnu + Rnu ahacṭag + Tutlayt n tsuffeɣt + Tazmilt-ik·im ɣef umiḍan-a + Kkes + Ih + Uhu + Werǧin + Ibdaren imaynuten + Ineqqisen + Arussin + Wayeḍ + Imiḍan imaynuten + Tugna + 1+ + tura + Sken akken yebɣu yili + deg %1$dsg + deg %1$dsn + Imaɣnuyen + Kkes azuzer + D aspam + Ayen-nniḍen + %1$s (%2$s) + Tettwaẓreg: %1$s + Tennulfa-d: %1$s + Asmizzwer n tɣuṛi + Azwel + Ffer-it + Rnu + Qbel + Sizdeg + Zgel + Qbel + Zgel + Tiririt + Abdar + Asedduklan + Qqen + Ilɣa + ALT + Tettwaẓreg ass n %1$s + Sken-d irewwayen + Azayez + War tabdart + D akessar + Ibenk-ik·im + Amiḍan-ik·im + Amidya: %1$s + Yettwasekles! \ No newline at end of file From cb6d8a65643dc15f2a4fed7450343ad9930a86c8 Mon Sep 17 00:00:00 2001 From: Moonshadow Date: Sun, 13 Apr 2025 13:07:14 +0000 Subject: [PATCH 20/54] Translated using Weblate (Kabyle) Currently translated at 54.3% (385 of 709 strings) Translated using Weblate (Kabyle) Currently translated at 53.3% (378 of 709 strings) Co-authored-by: Moonshadow Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/kab/ Translation: Tusky/Tusky --- app/src/main/res/values-kab/strings.xml | 65 ++++++++++++++++++++----- 1 file changed, 54 insertions(+), 11 deletions(-) diff --git a/app/src/main/res/values-kab/strings.xml b/app/src/main/res/values-kab/strings.xml index 010b3deff..6841cdbb0 100644 --- a/app/src/main/res/values-kab/strings.xml +++ b/app/src/main/res/values-kab/strings.xml @@ -11,16 +11,16 @@ Ɣef Tabdart Tabdarin - Izen-ik·im aṭas i ɣezzif! + Tasuffeɣt ɣezzifet aṭas! Agejdan Iccaren Asqerdec - Iznan + Tisuffaɣ S tririyin Ẓreg amaɣnu-ik \@%1$s - Wali ugar - Wali kra kan + Sken ugar + Sken drus Ulac walu da. %1$s yeṭṭafar-ik·ikem-id Err @@ -107,13 +107,13 @@ Imiḍanen yettwasgugmen Imiḍan yettusḥebsen Tiɣula yettwaffren - Isuturen n teḍfeṛt + Ḍfer issutar Tilɣa Taɣwalt Kkes Azen Ẓreg - Yettwanṭḍen + Yettwasenteḍ Rnu amidya Rnu assenqed Ṭef tugna @@ -149,7 +149,7 @@ Apṛuksi HTTP Tansa n upṛuksi HTTP Imeḍfaṛen imaynuten - Adɣaren + Igmaḍ n ufmiḍi Yuder-ik-id %1$s Yettwargel umiḍan %1$dis aya @@ -239,7 +239,7 @@ Adigan Turagin Yebḍa-t %1$s - %1$s yebḍa izen-ik·im + %1$s yebḍa tasuffeɣt-ik·im %1$s yerna izen-ik·im ɣer ismenyafen-is Tiririt taruradt Zuzer @@ -267,7 +267,7 @@ Asentel n wesnas Ulac ɣur-k·m ula d yiwet n tjewwiqt yettwasɣawsen. Tijewwiqin yettwasɣawsen - Tijewwiqin yettwasɣawsen + Tisuffaɣ yettwasɣawsen Sken-d izuzar Ihacṭagen Isuturen n teḍfeṛt @@ -360,11 +360,11 @@ Zgel Tiririt Abdar - Asedduklan + S lmendad Qqen Ilɣa ALT - Tettwaẓreg ass n %1$s + Tettwaẓreg%1$s Sken-d irewwayen Azayez War tabdart @@ -373,4 +373,47 @@ Amiḍan-ik·im Amidya: %1$s Yettwasekles! + Yettwasmenyaf + Tuzna… + Tuzna tettwasefsex + Tanzagt + Senqed ilɣa + Sefsex amulteɣ + Yettwakkes urewway + Suddes tsuffeɣt + Kkes asgugem i usqerdec + Kkesasgugem %1$s + Sader amkdya + Asadar n umidya + S\'en isem + Alɣu + <arameɣtu> + Hraw + Ahrawan + Yl tikkelt + Tuzna n tsuffaɣ + Yettwasmenyaf sɣur + Snes + Iwenniten niḍen + Afmiḍi + Kkes asgugem %1$s + Yensa + Imeddayen + Snirem + Alugan + Iznan usriden + Yeẓreg + Yerra-d + Kkes ticreḍt n usebter + Asmenyaf + Fneẓ + Iwenniten niḍen ? + Kkes asmenyaf + Tuzna n tsuffeɣt… + (Ulac beddel) + Asekles n urewway… + Urṭṭafar ara #%1$s? + Amidya yeffren + Ihacṭagen yettwaḍefren + Ẓreɣ tugna \ No newline at end of file From ad4d4cec6c65bb78ed208e147513a3b2243aaeba Mon Sep 17 00:00:00 2001 From: Moonshadow Date: Sun, 13 Apr 2025 13:07:14 +0000 Subject: [PATCH 21/54] Translated using Weblate (Kabyle) Currently translated at 55.4% (393 of 709 strings) Co-authored-by: Moonshadow Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/kab/ Translation: Tusky/Tusky --- app/src/main/res/values-kab/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/src/main/res/values-kab/strings.xml b/app/src/main/res/values-kab/strings.xml index 6841cdbb0..0803ecb40 100644 --- a/app/src/main/res/values-kab/strings.xml +++ b/app/src/main/res/values-kab/strings.xml @@ -416,4 +416,12 @@ Amidya yeffren Ihacṭagen yettwaḍefren Ẓreɣ tugna + Tuzna n yimezwer + Aṭas n ufran + rnu tasedmirt + Awal ummid + Aɣanib n yimujit + Tasuffeɣt yettwaẓergen + Aḍfarvyettwaqbal + %1$s yilugan \ No newline at end of file From 97efda2668e000a5d889645d0b0ac78dfcc6129b Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Mon, 14 Apr 2025 20:26:35 +0200 Subject: [PATCH 22/54] update shrinker rules and packaging excludes (#5055) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We wouldn't want to ship unnecessary stuff, would we apk size 20.788.307 -> 20.768.841 bytes 🤪 --- app/build.gradle | 3 +++ app/proguard-rules.pro | 34 ++++++++++++++-------------------- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index f8258d8e8..4615ded61 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -105,6 +105,9 @@ android { packagingOptions.resources.excludes += [ 'LICENSE_OFL', 'LICENSE_UNICODE', + 'META-INF/androidx/**', + 'META-INF/NOTICE.md', + 'DebugProbesKt.bin' ] bundle { diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 0bcca6c2a..b3833ebce 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -20,23 +20,6 @@ public static final ** CREATOR; } -# Preserve annotated Javascript interface methods. --keepclassmembers class * { - @android.webkit.JavascriptInterface ; -} - -# The support libraries contains references to newer platform versions. -# Don't warn about those in case this app is linking against an older -# platform version. We know about them, and they are safe. --dontnote androidx.** --dontwarn androidx.** - -# This class is deprecated, but remains for backward compatibility. --dontwarn android.util.FloatMath - -# These classes are duplicated between android.jar and core-lambda-stubs.jar. --dontnote java.lang.invoke.** - # TUSKY SPECIFIC OPTIONS # preserve line numbers for crash reporting @@ -47,6 +30,10 @@ -keep class org.bouncycastle.jcajce.provider.asymmetric.EC$* { *; } -keep class org.bouncycastle.jcajce.provider.asymmetric.ec.KeyPairGeneratorSpi$EC +# Preference fragments can be referenced by name, ensure they remain +# https://github.com/tuskyapp/Tusky/issues/3161 +-keep class * extends androidx.preference.PreferenceFragmentCompat + # remove all logging from production apk -assumenosideeffects class android.util.Log { public static *** getStackTraceString(...); @@ -70,9 +57,16 @@ static void checkNotNullExpressionValue(java.lang.Object, java.lang.String); static void checkReturnedValueIsNotNull(java.lang.Object, java.lang.String); static void checkReturnedValueIsNotNull(java.lang.Object, java.lang.String, java.lang.String); + static void throwUninitializedProperty(java.lang.String); static void throwUninitializedPropertyAccessException(java.lang.String); } -# Preference fragments can be referenced by name, ensure they remain -# https://github.com/tuskyapp/Tusky/issues/3161 --keep class * extends androidx.preference.PreferenceFragmentCompat +# there is no need for edit mode in production builds, allow it to be pruned +-assumenosideeffects public class * extends android.view.View { + boolean isInEditMode(); +} +-assumevalues public class * extends android.view.View { + boolean isInEditMode() return false; +} + +-checkdiscard class com.keylesspalace.tusky.usecase.DeveloperToolsUseCase From 41795c7fba159c662c938f5c5ebfb3358de80105 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Mon, 14 Apr 2025 20:26:51 +0200 Subject: [PATCH 23/54] fix undead MainActivity appearing after browser login and back press (#5056) To reproduce, login an additional account via Browser login and then press back. You will find yourself in a broken interface where the displayed posts don't match the logged-in account. This does not happen with Tusky login. Seems like `finishAffinity()` does not work as expected when there is an Activity of another app (the browser) on the stack. Fix with the nuclear option (starting a new task). Also remove the explode animation, as this was broken before (finish needs to be called after startActivity for the animation to appear) and animations between tasks don't work in newer Android versions anyway. --- .../java/com/keylesspalace/tusky/MainActivity.kt | 16 ---------------- .../tusky/components/login/LoginActivity.kt | 3 +-- app/src/main/res/anim/explode.xml | 12 ------------ 3 files changed, 1 insertion(+), 30 deletions(-) delete mode 100644 app/src/main/res/anim/explode.xml diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt index 99f98dcf7..9e68db998 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt @@ -95,11 +95,9 @@ import com.keylesspalace.tusky.pager.MainPagerAdapter import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.usecase.DeveloperToolsUseCase import com.keylesspalace.tusky.usecase.LogoutUsecase -import com.keylesspalace.tusky.util.ActivityConstants import com.keylesspalace.tusky.util.emojify import com.keylesspalace.tusky.util.getParcelableExtraCompat import com.keylesspalace.tusky.util.hide -import com.keylesspalace.tusky.util.overrideActivityTransitionCompat import com.keylesspalace.tusky.util.reduceSwipeSensitivity import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation @@ -217,14 +215,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider { viewModel.setupNotifications(this) } - if (explodeAnimationWasRequested()) { - overrideActivityTransitionCompat( - ActivityConstants.OVERRIDE_TRANSITION_OPEN, - R.anim.explode, - R.anim.activity_open_exit - ) - } - selectedEmojiPack = preferences.getString(EMOJI_PREFERENCE, "") var showNotificationTab = false @@ -1118,15 +1108,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider { } } - private fun explodeAnimationWasRequested(): Boolean { - return intent.getBooleanExtra(OPEN_WITH_EXPLODE_ANIMATION, false) - } - override fun getActionButton() = binding.composeButton companion object { - const val OPEN_WITH_EXPLODE_ANIMATION = "explode" - private const val TAG = "MainActivity" // logging tag private const val DRAWER_ITEM_ADD_ACCOUNT: Long = -13 private const val DRAWER_ITEM_ANNOUNCEMENTS: Long = 14 diff --git a/app/src/main/java/com/keylesspalace/tusky/components/login/LoginActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/login/LoginActivity.kt index 9bd35af3e..33c604755 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/login/LoginActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/login/LoginActivity.kt @@ -291,9 +291,8 @@ class LoginActivity : BaseActivity() { oauthScopes = OAUTH_SCOPES, newAccount = newAccount ) - finishAffinity() val intent = Intent(this, MainActivity::class.java) - intent.putExtra(MainActivity.OPEN_WITH_EXPLODE_ANIMATION, true) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) startActivity(intent) }, { e -> setLoading(false) diff --git a/app/src/main/res/anim/explode.xml b/app/src/main/res/anim/explode.xml deleted file mode 100644 index 08001aef5..000000000 --- a/app/src/main/res/anim/explode.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - From 3c7bc2de749ab1aef22c6746f7217ab6e84797b8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 14 Apr 2025 20:28:13 +0200 Subject: [PATCH 24/54] Update androidx.media3 to v1.6.1 (#5059) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [androidx.media3:media3-ui](https://redirect.github.com/androidx/media) | `1.6.0` -> `1.6.1` | [![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.media3:media3-ui/1.6.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.media3:media3-ui/1.6.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.media3:media3-ui/1.6.0/1.6.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.media3:media3-ui/1.6.0/1.6.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [androidx.media3:media3-datasource-okhttp](https://redirect.github.com/androidx/media) | `1.6.0` -> `1.6.1` | [![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.media3:media3-datasource-okhttp/1.6.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.media3:media3-datasource-okhttp/1.6.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.media3:media3-datasource-okhttp/1.6.0/1.6.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.media3:media3-datasource-okhttp/1.6.0/1.6.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [androidx.media3:media3-exoplayer](https://redirect.github.com/androidx/media) | `1.6.0` -> `1.6.1` | [![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.media3:media3-exoplayer/1.6.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.media3:media3-exoplayer/1.6.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.media3:media3-exoplayer/1.6.0/1.6.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.media3:media3-exoplayer/1.6.0/1.6.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
androidx/media (androidx.media3:media3-ui) ### [`v1.6.1`](https://redirect.github.com/androidx/media/blob/HEAD/RELEASENOTES.md#161-2025-04-14) [Compare Source](https://redirect.github.com/androidx/media/compare/1.6.0...1.6.1) This release includes the following changes since the [1.6.0 release](#​160-2025-03-26): - Common Library: - Add `PlaybackParameters.withPitch(float)` method for easily copying a `PlaybackParameters` with a new `pitch` value ([#​2257](https://redirect.github.com/androidx/media/issues/2257)). - ExoPlayer: - Fix issue where media item transition fails due to recoverable renderer error during initialization of the next media item ([#​2229](https://redirect.github.com/androidx/media/issues/2229)). - Fix issue where `ProgressiveMediaPeriod` throws an `IllegalStateException` as `PreloadMediaSource` attempts to call its `getBufferedDurationUs()` before it is prepared ([#​2315](https://redirect.github.com/androidx/media/issues/2315)). - Fix sending `CmcdData` in manifest requests for DASH, HLS, and SmoothStreaming ([#​2253](https://redirect.github.com/androidx/media/pull/2253)). - Ensure `AdPlaybackState.withAdDurationsUs(long[][])` can be used after ad groups have been removed. The user still needs to pass in an array of durations for removed ad groups which can be empty or null ([#​2267](https://redirect.github.com/androidx/media/issues/2267)). - Extractors: - MP4: Parse `alternate_group` from the `tkhd` box and expose it as an `Mp4AlternateGroupData` entry in each track's `Format.metadata` ([#​2242](https://redirect.github.com/androidx/media/issues/2242)). - Audio: - Fix offload issue where the position might get stuck when playing a playlist of short content ([#​1920](https://redirect.github.com/androidx/media/issues/1920)). - Session: - Lower aggregation timeout for platform `MediaSession` callbacks from 500 to 100 milliseconds and add an experimental setter to allow apps to configure this value. - Fix issue where notifications reappear after they have been dismissed by the user ([#​2302](https://redirect.github.com/androidx/media/issues/2302)). - Fix a bug where the session returned a single-item timeline when the wrapped player is actually empty. This happened when the wrapped player doesn't have `COMMAND_GET_TIMELINE` available while `COMMAND_GET_CURRENT_MEDIA_ITEM` is available and the wrapped player is empty ([#​2320](https://redirect.github.com/androidx/media/issues/2320)). - Fix a bug where calling `MediaSessionService.setMediaNotificationProvider` is silently ignored after other interactions with the service like `setForegroundServiceTimeoutMs` ([#​2305](https://redirect.github.com/androidx/media/issues/2305)). - UI: - Enable `PlayerSurface` to work with `ExoPlayer.setVideoEffects` and `CompositionPlayer`. - Fix bug where `PlayerSurface` can't be recomposed with a new `Player`. - HLS extension: - Fix issue where chunk duration wasn't set in `CmcdData` for HLS media, causing an assertion failure when processing encrypted media segments ([#​2312](https://redirect.github.com/androidx/media/issues/2312)). - RTSP extension: - Add support for URI with RTSPT scheme as a way to configure the RTSP session to use TCP ([#​1484](https://redirect.github.com/androidx/media/issues/1484)). - Cast extension: - Add support for playlist metadata ([#​2235](https://redirect.github.com/androidx/media/pull/2235)).
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about these updates again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/tuskyapp/Tusky). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- gradle/verification-metadata.xml | 72 ++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f310eac4b..26d915322 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,7 +12,7 @@ androidx-fragment = "1.8.6" androidx-hilt = "1.2.0" androidx-junit = "1.2.1" androidx-lifecycle = "2.8.7" -androidx-media3 = "1.6.0" +androidx-media3 = "1.6.1" androidx-paging = "3.3.6" androidx-preference = "1.2.1" androidx-recyclerview = "1.4.0" diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 17dc9e350..f857131c1 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2499,6 +2499,14 @@
+ + + + + + + + @@ -2539,6 +2547,14 @@ + + + + + + + + @@ -2579,6 +2595,14 @@ + + + + + + + + @@ -2619,6 +2643,14 @@ + + + + + + + + @@ -2662,6 +2694,14 @@ + + + + + + + + @@ -2702,6 +2742,14 @@ + + + + + + + + @@ -2745,6 +2793,14 @@ + + + + + + + + @@ -2785,6 +2841,14 @@ + + + + + + + + @@ -2828,6 +2892,14 @@ + + + + + + + + From 8ad5609aef2ed29786baa308f4ba74a6a9c70ffd Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Tue, 15 Apr 2025 07:50:40 +0200 Subject: [PATCH 25/54] fix activity restart when emoji pack changed (#5057) When the emoji pack changes and `MainActivity` is recreated status bar and navigation bar turn dark in edge-to-edge mode (Android 15), we would need [the same workaround as here](https://github.com/tuskyapp/Tusky/blob/068d884aefcd954b72cb5f13bf569b95c9da2ebe/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt#L71-L75) in `MainActivity` as well. But why not handle the restart in `AccountActivity` as we do for all other preferences that require it. --- .../com/keylesspalace/tusky/MainActivity.kt | 20 ------------------- .../preference/PreferencesActivity.kt | 3 ++- 2 files changed, 2 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt index 9e68db998..3c3ac2fd6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt @@ -126,7 +126,6 @@ import com.mikepenz.materialdrawer.util.updateBadge import com.mikepenz.materialdrawer.widget.AccountHeaderView import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.migration.OptionalInject -import de.c1710.filemojicompat_ui.helpers.EMOJI_PREFERENCE import javax.inject.Inject import kotlinx.coroutines.launch @@ -162,9 +161,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider { private var onTabSelectedListener: OnTabSelectedListener? = null - // We need to know if the emoji pack has been changed - private var selectedEmojiPack: String? = null - /** Mediate between binding.viewPager and the chosen tab layout */ private var tabLayoutMediator: TabLayoutMediator? = null @@ -215,8 +211,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider { viewModel.setupNotifications(this) } - selectedEmojiPack = preferences.getString(EMOJI_PREFERENCE, "") - var showNotificationTab = false // check for savedInstanceState in order to not handle intent events more than once @@ -478,20 +472,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider { } } - override fun onResume() { - super.onResume() - val currentEmojiPack = preferences.getString(EMOJI_PREFERENCE, "") - if (currentEmojiPack != selectedEmojiPack) { - Log.d( - TAG, - "onResume: EmojiPack has been changed from %s to %s" - .format(selectedEmojiPack, currentEmojiPack) - ) - selectedEmojiPack = currentEmojiPack - recreate() - } - } - override fun dispatchKeyEvent(event: KeyEvent): Boolean { // Allow software back press to be properly dispatched to drawer layout val handled = when (event.action) { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt index 9a71d34bd..0d994f705 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt @@ -41,6 +41,7 @@ import com.keylesspalace.tusky.util.getNonNullString import com.keylesspalace.tusky.util.setAppNightMode import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation import dagger.hilt.android.AndroidEntryPoint +import de.c1710.filemojicompat_ui.helpers.EMOJI_PREFERENCE import javax.inject.Inject import kotlinx.coroutines.launch @@ -159,7 +160,7 @@ class PreferencesActivity : } PrefKeys.STATUS_TEXT_SIZE, PrefKeys.ABSOLUTE_TIME_VIEW, PrefKeys.SHOW_BOT_OVERLAY, PrefKeys.ANIMATE_GIF_AVATARS, PrefKeys.USE_BLURHASH, PrefKeys.SHOW_SELF_USERNAME, PrefKeys.SHOW_CARDS_IN_TIMELINES, PrefKeys.CONFIRM_REBLOGS, PrefKeys.CONFIRM_FAVOURITES, - PrefKeys.ENABLE_SWIPE_FOR_TABS, PrefKeys.MAIN_NAV_POSITION, PrefKeys.HIDE_TOP_TOOLBAR, PrefKeys.SHOW_STATS_INLINE -> { + EMOJI_PREFERENCE, PrefKeys.ENABLE_SWIPE_FOR_TABS, PrefKeys.MAIN_NAV_POSITION, PrefKeys.HIDE_TOP_TOOLBAR, PrefKeys.SHOW_STATS_INLINE -> { restartActivitiesOnBackPressedCallback.isEnabled = true } } From 44867b0fda7fbe330822b6932354b3f18cf6430a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=E1=BB=93=20Nh=E1=BA=A5t=20Duy?= Date: Thu, 17 Apr 2025 16:07:14 +0000 Subject: [PATCH 26/54] Translated using Weblate (Vietnamese) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (709 of 709 strings) Translated using Weblate (Vietnamese) Currently translated at 100.0% (709 of 709 strings) Co-authored-by: Hồ Nhất Duy Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/vi/ Translation: Tusky/Tusky --- app/src/main/res/values-vi/strings.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 992b7c7e7..79c7755b2 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -648,10 +648,10 @@ \n \nVí dụ: bạn có thể xem hồ sơ của một người và nhấn vào nút [iconics gmd_edit] và đổi kiểu tút. \u0020
Đây là danh sách. Bạn có thể tạo nhiều danh sách riêng tư và thêm người dùng vào đó. -\n -\nChỉ những người BẠN ĐANG THEO DÕI mới có thể thêm vào danh sách. -\n -\nDanh sách có thể được sử dụng như một tab trong thiết lập {manage_accounts}} Cá nhân {{chevron_right}} Xếp tab. +\n \u0020 +\nChỉ những người BẠN ĐANG THEO DÕI mới có thể thêm vào danh sách. \u0020 +\n \u0020 +\nDanh sách có thể được sử dụng như một tab trong thiết lập {{manage_accounts}} Cá nhân {{chevron_right}} Xếp tab.
Đang ẩn hashtag #%1$s như một cảnh báo Đang bỏ ẩn hashtag #%1$s Xem bộ lọc From a6154c4ccd078280d279d4221c676ad66f5d70e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sveinn=20=C3=AD=20Felli?= Date: Thu, 17 Apr 2025 16:07:14 +0000 Subject: [PATCH 27/54] Translated using Weblate (Icelandic) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (709 of 709 strings) Co-authored-by: Sveinn í Felli Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/is/ Translation: Tusky/Tusky --- app/src/main/res/values-is/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index 2101faca7..d4af50b5f 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -775,4 +775,12 @@ Nei Henda breytingum á skýringatexta? + Aflæsti %1$s + Hætti að þagga %1$s + Mistókst að þagga %1$s: %2$s + Mistókst að aflæsa %1$s: %2$s + Mistókst að afþagga %1$s: %2$s + Mistókst að samþykkja beiðni um að fylgjast með frá %1$s: %2$s + Mistókst að loka á %1$s: %2$s + Mistókst að hafna beiðni um að fylgjast með frá %1$s: %2$s \ No newline at end of file From 91bb19cd4cebbbc590aa952fe0adf9dce2271875 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 18 Apr 2025 08:57:52 +0200 Subject: [PATCH 28/54] Update hilt to v2.56.2 (#5064) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [com.google.dagger.hilt.android](https://redirect.github.com/google/dagger) | `2.56.1` -> `2.56.2` | [![age](https://developer.mend.io/api/mc/badges/age/maven/com.google.dagger.hilt.android:com.google.dagger.hilt.android.gradle.plugin/2.56.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.google.dagger.hilt.android:com.google.dagger.hilt.android.gradle.plugin/2.56.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.google.dagger.hilt.android:com.google.dagger.hilt.android.gradle.plugin/2.56.1/2.56.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.google.dagger.hilt.android:com.google.dagger.hilt.android.gradle.plugin/2.56.1/2.56.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [com.google.dagger:hilt-compiler](https://redirect.github.com/google/dagger) | `2.56.1` -> `2.56.2` | [![age](https://developer.mend.io/api/mc/badges/age/maven/com.google.dagger:hilt-compiler/2.56.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.google.dagger:hilt-compiler/2.56.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.google.dagger:hilt-compiler/2.56.1/2.56.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.google.dagger:hilt-compiler/2.56.1/2.56.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [com.google.dagger:hilt-android](https://redirect.github.com/google/dagger) | `2.56.1` -> `2.56.2` | [![age](https://developer.mend.io/api/mc/badges/age/maven/com.google.dagger:hilt-android/2.56.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.google.dagger:hilt-android/2.56.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.google.dagger:hilt-android/2.56.1/2.56.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.google.dagger:hilt-android/2.56.1/2.56.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about these updates again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/tuskyapp/Tusky). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- gradle/verification-metadata.xml | 95 ++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 26d915322..29d5985bf 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -31,7 +31,7 @@ emoji2 = "1.4.0" filemoji-compat = "3.2.7" glide = "4.16.0" glide-animation-plugin = "3.0.4" -hilt = "2.56.1" +hilt = "2.56.2" kotlin = "2.1.20" image-cropper = "4.3.2" material = "1.12.0" diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index f857131c1..00d77dee5 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -12444,6 +12444,14 @@ + + + + + + + + @@ -12508,6 +12516,14 @@ + + + + + + + + @@ -12572,6 +12588,14 @@ + + + + + + + + @@ -12636,6 +12660,14 @@ + + + + + + + + @@ -12703,6 +12735,14 @@ + + + + + + + + @@ -12767,6 +12807,14 @@ + + + + + + + + @@ -12831,6 +12879,14 @@ + + + + + + + + @@ -12898,6 +12954,14 @@ + + + + + + + + @@ -12938,6 +13002,11 @@ + + + + + @@ -13740,6 +13809,14 @@ + + + + + + + + @@ -13791,6 +13868,11 @@ + + + + + @@ -13910,6 +13992,14 @@ + + + + + + + + @@ -13967,6 +14057,11 @@ + + + + + From 82a86ada8e2f2a46cb885eab8a977495fd5a9daa Mon Sep 17 00:00:00 2001 From: Danial Behzadi Date: Tue, 22 Apr 2025 02:07:23 +0000 Subject: [PATCH 29/54] Translated using Weblate (Persian) Currently translated at 100.0% (709 of 709 strings) Co-authored-by: Danial Behzadi Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/fa/ Translation: Tusky/Tusky --- app/src/main/res/values-fa/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index a8f518aeb..018294994 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -55,7 +55,7 @@ ورود با تاسکی خروج پیگیری - ناپیگیری + ناپی‌گیری انسداد رفع انسداد نهفتن تقویت‌ها @@ -125,7 +125,7 @@ در حال بارگذاری… بارگیری درخواست دنبال کردن را لغو می‌کنید؟ - ناپیگیری این حساب؟ + ناپی‌گیری این حساب؟ حذف این فرسته؟ عمومی: فرستادن به خط زمانی‌های عمومی فهرست‌نشده: نشان ندادن در خط زمانی‌های عمومی From 9cfe172c913857f06a56428ffbb94f40ba22efdc Mon Sep 17 00:00:00 2001 From: Vladyslav Stepanov Date: Tue, 22 Apr 2025 02:07:23 +0000 Subject: [PATCH 30/54] Translated using Weblate (Ukrainian) Currently translated at 100.0% (709 of 709 strings) Translated using Weblate (Russian) Currently translated at 100.0% (709 of 709 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (709 of 709 strings) Translated using Weblate (Russian) Currently translated at 100.0% (709 of 709 strings) Translated using Weblate (Russian) Currently translated at 100.0% (709 of 709 strings) Co-authored-by: Vladyslav Stepanov Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/ru/ Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/uk/ Translation: Tusky/Tusky --- app/src/main/res/values-ru/strings.xml | 166 +++++++++++---------- app/src/main/res/values-uk/strings.xml | 194 ++++++++++++++----------- 2 files changed, 200 insertions(+), 160 deletions(-) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 20e8baf24..74e6cf8fd 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -1,7 +1,7 @@ Возникла ошибка. - Произошла ошибка сети. Пожалуйста, проверьте интернет-соединение и попробуйте снова. + Произошла сетевая ошибка. Пожалуйста, проверьте подключение и повторите попытку. Не может быть пустым. Введен недопустимый домен Не удалось пройти аутентификацию на этом сервере. Если ошибка повторяется каждый раз, попробуйте войти через браузер, выбрав соответствующий пункт в меню. @@ -19,7 +19,7 @@ Ошибка при отправке сообщения. Главная Уведомления - Местная лента + Этот сервер Объединенная лента Личные сообщения Вкладки @@ -30,7 +30,7 @@ Подписки Подписчики Избранное - Заглушенные пользователи + Игнорируемые пользователи Заблокированные пользователи Запросы на подписку Редактировать профиль @@ -41,7 +41,7 @@ Содержание, которое может быть неприятным или нежелательным Медиа скрыто Нажмите для просмотра - Показать + Показать еще Свернуть Еще Свернуть @@ -56,7 +56,7 @@ Ответить Продвинуть Отменить продвижение - Избрать + Нравится Убрать из избранного Больше Написать @@ -80,7 +80,7 @@ Настройки Настройки учетной записи Избранное - Заглушенные пользователи + Игнорируемые пользователи Заблокированные пользователи Запросы на подписку Медиа @@ -88,8 +88,8 @@ Добавить медиа Сделать снимок Поделиться - Заглушить - Не глушить + Игнорировать + Перестать игнорировать Упомянуть Скрыть медиа Открыть всплывающее меню @@ -126,7 +126,7 @@ Поделится медиа в… Отправлено! Пользователь разблокирован - Глушение пользователя снято + Игнорирование пользователя снято Какой сервер? Что случилось\? Предупреждение о содержании @@ -139,12 +139,12 @@ Заголовок Что такое сервер? Подключаюсь… - Здесь можно указать адрес или домен любого сервера, например mastodon.social, icosahedron.website, social.tchncs.de или какого-нибудь ещё! -\n -\nЕсли у вас еще нет учетной записи, вы можете ввести название сервера, к которому хотите присоединиться, и создать на нем новую учетную запись. -\n -\nСервер — это тот сайт, на котором располагается ваша учетная запись, но вы можете легко общаться с людьми на других серверах и подписываться на них, как будто вы находитесь на одном и том же сайте. -\n + Здесь можно указать адрес или домен любого сервера, например mastodon.social, icosahedron.website, social.tchncs.de или какого-нибудь ещё! +\n +\nЕсли у вас еще нет учетной записи, вы можете ввести название сервера, к которому хотите присоединиться, и создать на нем новую учетную запись. +\n +\nСервер — это тот сайт, на котором располагается ваша учетная запись, но вы можете легко общаться с людьми на других серверах и подписываться на них, как будто вы находитесь на одном и том же сайте. +\n \nБолее подробную информацию можно найти на сайте joinmastodon.org. Завершение загрузки медиа Загружаем… @@ -187,8 +187,8 @@ Адрес HTTP прокси сервера Порт Видимость постов (синхронизируется с сервером) - Всегда помечать медиафайлы как деликатные (синхронизируется с сервером) - Установки по умолчанию для новых постов + Всегда помечать медиа как чувствительные (синхронизируется с сервером) + Установки для новых публикаций по умолчанию Не удалось синхронизировать настройки Публичный Скрытый @@ -329,7 +329,7 @@ Загрузка не удалась Бот %1$s переехал(а) на: - Процитировать для изначальной аудитории + Продвинуть для изначальной аудитории Отменить продвижение Tusky использует код и материалы из следующих проектов с открытым исходным кодом: Используется лицензия Apache License (копия ниже) @@ -337,17 +337,17 @@ CC-BY-SA 4.0 Метаданные профиля добавить данные - Ключ + Метка Значение - Показывать абсолютное время + Показывать точное время Информация ниже может отображать профиль пользователя не полностью. Нажмите, чтобы открыть полный профиль в браузере. Открепить - Прикрепить + Закрепить - %1$s лайк - %1$s лайка - %1$s лайков - %1$s лайков + %1$s оценил + %1$s оценили + %1$s оценили + %1$s оценили %1$s продвижение @@ -363,7 +363,7 @@ Медиа: %1$s Предупреждение о содержании: %1$s Без описания - Распространено + Перезалито Понравилось Публичный Скрытый @@ -377,7 +377,7 @@ Создать пост Написать Показывать индикаторы для ботов - Вы точно хотите удалить навсегда все ваши уведомления\? + Вы уверены, что хотите навсегда очистить все уведомления? \u0020 \u0020%1$s • %2$s %1$s голос @@ -396,7 +396,7 @@ Дополнительные замечания Скрытые домены Скрытые домены - Заглушить %1$s + Игнорировать %1$s %1$s больше не скрыт Вы уверены, что хотите заблокировать весь %1$s? Вы не увидите контент с этого домена ни в публичных лентах, ни в своих уведомлениях. Ваши подписчики с этого домена будут удалены. Скрыть весь домен @@ -410,8 +410,8 @@ Переслать в %1$s Не удалось пожаловаться Не удалось получить посты - Жалоба будет отправлена модераторам вашего сервера. Ниже вы можете дать объяснение о причинах жалобы на эту учетную запись: - Эта учетная запись расположена на другом сервере. Переслать туда анонимную копию жалобы? + Жалоба будет отправлена модераторам вашего сервера. Ниже вы можете дать объяснение почему вы жалуетесь на эту учетную запись: + Аккаунт находится на другом сервере. Отправить анонимную копию отчета также туда? Добавить опрос Всегда раскрывать посты с предупреждениями о содержании Профили @@ -429,7 +429,7 @@ Вариант %1$d Изменить Запланированные посты - Править + Редактировать Запланированные посты Запланировать пост Сброс @@ -446,7 +446,7 @@ Минимальный интервал планирования в Mastodon составляет 5 минут. Запрашивать подтверждение перед продвижением Показывать предпросмотр ссылок в ленте - Включить переключение между вкладками свайпом + Включить жест пролистывания для переключения между вкладками %1$s человек %1$s человека @@ -455,10 +455,10 @@ Уведомления о запросах на подписку Запросы на подписку - Заглушить @%1$s? + Игнорировать @%1$s? Заблокировать @%1$s? - Не глушить обсуждение - Заглушить обсуждение + Не игнорировать обсуждение + Игнорировать обсуждение %1$s отправил(а) вам запрос на подписку Хэштеги Добавить хэштэг @@ -466,7 +466,7 @@ Снизу Сверху Расположение панели навигации - Разблокировать %1$s + Не игнорировать %1$s Скрыть уведомления Перестать игнорировать %1$s Сохранено! @@ -521,8 +521,8 @@ Вход через Браузер Изображение не удалось отредактировать. Размер видео и аудио не может превышать %1$s МБ. - Не удалось заглушить #%1$s - Этот сервер не поддерживает подписку на хештеги. + Не удалось игнорировать #%1$s + Этот сервер не поддерживает следующие хештеги. Не удалось подписаться на #%1$s Не удалось отписаться от #%1$s Загрузка не удалась: %1$s @@ -541,7 +541,7 @@ У вас нет никаких списков. Удалить этот запланированный пост\? Отменить подписку на #%1$s? - Не удалось прикрепить + Не удалось закрепить Изображение Показать в любом случае Отфильтровано: %1$s @@ -556,7 +556,7 @@ Получаю уведомления… Работа в фоне пользователей - Язык записи + Язык Поста Язык постов (синхронизируется с сервером) Уведомлять о редактировании постов в которых вы участвовали Правки постов @@ -582,7 +582,7 @@ Подробности Отклонить У медиа должно быть описание. - Если вы вошли в несколько учетных записей + Когда в систему входит несколько учетных записей Сохраняю черновик… Профили Не удалось добавить учетную запись в список @@ -657,26 +657,26 @@ Не удалось загрузить источник состояния с сервера. Добавить или удалить из списка Не удалось открепить - Заглушить хэштег #%1$s с предупреждением - Хэштег #%1$s больше не скрывается + Игнорировать хэштег #%1$s в качестве предупреждения + Хэштег #%1$s больше не игнорируется Посмотреть фильтр Теперь хэштег #%1$s отслеживается Хэштег #%1$s больше не отслеживается Нарушение правил Установить точку фокуса - Это ваша домашняя лента. Здесь отображаются последние сообщения от пользователей, на которых вы подписаны. -\n + Это ваша домашняя лента. Здесь отображаются последние сообщения от пользователей, на которых вы подписаны. +\n \nЧтобы найти людей, вы можете обнаружить профили в других лентах. Например, в местной ленте вашего сервера {{group}}. Либо вы можете поискать их по имени {{search}}; например, по имени Tusky вы найдете наш профиль Mastodon. - Здесь находятся ваши личные сообщения; иногда их называют беседами или direct messages (DM). -\n -\nЛичные сообщения создаются путем установки видимости {{public}} поста на {{mail}} Личное упоминание и упоминания одного или нескольких пользователей в тексте. -\n + Здесь находятся ваши частные сообщения; иногда их называют беседами или прямыми сообщениями (DM). +\n +\nЛичные сообщения создаются путем установки видимости {{public}} поста на {{mail}} Личное упоминание и упоминания одного или нескольких пользователей в тексте. +\n \nНапример, вы можете зайти в профиль пользователя, нажать кнопку создания поста [iconics gmd_edit] и изменить видимость. - Это ваш раздел списков. Вы можете создавать личные списки и добавлять в них пользователей. -\n -\nОбратите внимание, что в списки можно добавлять только тех пользователей, на которые вы подписаны. -\n -\nЭти списки можно использовать в качестве вкладок в меню Настройки учетной записи {{manage_accounts}} {{chevron_right}} Вкладки. + Это ваш раздел списков. Вы можете создавать личные списки и добавлять в них пользователей. +\n +\nОбратите внимание, что в списки можно добавлять только тех пользователей, на которых вы подписаны. +\n +\nЭти списки можно использовать в качестве вкладки в меню Настройки учетной записи {{manage_accounts}} {{chevron_right}} Вкладки. У вас есть несохраненные изменения. 1+ Сохранить черновик\? (Вложения будут загружены снова при восстановлении черновика). @@ -685,13 +685,13 @@ Показывать статистику постов в ленте Скрыть с предупреждением неизвестная причина - Не удалось заглушить %1$s: %2$s - Не удалось вернуть звук %1$s: %2$s + Не удалось скрыть %1$s: %2$s + Не удалось перестать игнорировать %1$s: %2$s Уведомлять о жалобах модерации Загрузка самых новых уведомлений Удалить черновик\? - Ваш сервер знает, что это сообщение было отредактировано, но у него нет копии правок, поэтому они не могут быть показаны. -\n + Ваш сервер знает, что это сообщение было отредактировано, но у него нет копии правок, поэтому они не могут быть вам показаны. +\n \nЭто баг Mastodon №25398. Никому Члены списка @@ -724,13 +724,13 @@ Отправка… Видимость ответов (не синхронизируется с сервером) Неизвестный тип уведомлений - Показывать панель фильтра уведомлений + Показать фильтр уведомлений Подписаться на пользователя? Запрашивать подтверждение перед подпиской на пользователя Нарушение закона Ошибка перевода: %1$s - Такая же, как видимость постов - Срок действия + Соответствует типичным параметрам приватности сообщений + Истекает после Подписаться на новый хэштег Отфильтрованные уведомления Принять запрос на показ уведомлений @@ -738,16 +738,16 @@ Фильтруются, если только это не ответ на ваше собственное упоминание или если вы подписаны на отправителя Включая людей, которые подписаны на вас меньше чем 3 дня Эта функция поддерживается только на серверах Mastodon версии 4.3.0 и выше. - Управление уведомлениями + Политика уведомлений Люди, на которых вы не подписаны Пока вы не одобрите их вручную Люди, не подписанные на вас Новые учётные записи Созданные в течение последних 30 дней Нежелательные личные упоминания - Модерируемые учётные записи + Учётные записи под руководством модератора Ограниченные модераторами сервера - Управление уведомлениями + Управление уведомлениями от… Открыть настройки Отфильтрованные уведомления Принимать @@ -760,9 +760,9 @@ Уведомления от %1$d людей, которых вы можете знать Не удалось удалить фильтр \'%1$s\' Не удалось сохранить фильтр \'%1$s\' - Прочие уведомления, например насчёт разорванных отношений или предупреждений от модерации + Прочие уведомления, например насчёт утраченных контактов или предупреждений от модерации Администрирование - Администраторы %1$s заблокировали %2$s, включая %3$s ваших подписчиков и %4$s подписок. + Администратор из %1$s заблокировал %2$s, включая %3$s ваших подписчиков и %4$s, на которых вы подписаны. %1$s учетная запись %1$s учетных записи @@ -779,35 +779,43 @@ %1$s медиафайлов Административные уведомления, например насчёт новых жалоб и регистраций - Администраторы %1$s заблокировали %2$s, а значит вы больше не сможете читать этот профиль или взаимодействовать с ним. - С этого момента ваши посты будут отмечены как контент деликатного характера. - Отношение разорвано + Администратор из %1$s приостановил действие %2$s, что означает, что вы больше не можете получать от него обновления или взаимодействовать с ним. + С этого момента ваши сообщения будут помечены как чувствительные. + Связь потеряна Ваша учетная запись получила предупреждение от модерации. - Tusky не может распознать тип этого уведомления, и потому не может его отобразить. Вы скорее всего сможете просмотреть это уведомление, воспользовавшись веб-интерфейсом вашего сервера. А если вы расскажете о проблеме разработчикам Tusky, они могут добавить поддержку таких уведомлений! + Tusky не распознает этот тип уведомления и не может показать его вам. Возможно, это может сделать веб-интерфейс вашего сервера. Пожалуйста, сообщите об этом разработчикам Tusky, чтобы они добавили поддержку этого типа! Показать результаты - Приватный ответ + Частный ответ Ответ - Приватное упоминание + Частное упоминание Упоминание - Ваша учетная запись была заблокирована. + Ваша учетная запись была приостановлена. Ваша учётная запись отключена. Ваша учетная запись была ограничена. Некоторые ваши посты были удалены. - Некоторые ваши посты были отмечены как контент деликатного характера. + Некоторые из ваших сообщений были помечены как чувствительные. Продвинуть для подписчиков Продвинуть публично - Продвинуть не очень публично - Разорванные отношения - Предупреждения от модерации + Скрытое продвижение + Утраченные связи + Предупреждения модерации Неизвестный тип уведомлений Прочее От %1$s Больше от %1$s Да Нет - Отбросить изменения? + Отбросить изменения надписи? Вы подписались на хэштег %1$s Ответ для %1$s - Продолжение цепочки + Продолжение обсуждения Ответил(а) + Игнорирование снято %1$s + Возврат из игнорирования потерпел крах %1$s: %2$s + Разблокировано %1$s + Не удалось заблокировать %1$s: %2$s + Не удалось разблокировать %1$s: %2$s + Не удалось принять запрос на подписку от %1$s: %2$s + Не удалось отклонить запрос на подписку от %1$s: %2$s + Не удалось скрыть %1$s: %2$s \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 1287b20a1..c64588e37 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -4,14 +4,14 @@ \@%1$s Ліцензії Редагувати профіль - Запити на стеження + Запити на підписку Закладки Підписники Підписки Прикріплені З відповідями Загальні - Локальні + Цей сервер Приватні повідомлення Сповіщення Головна @@ -61,22 +61,22 @@ Змінити Зберегти Згадати - Сховати %1$s - Сховати + Ігнорувати %1$s + Ігнорувати Поділитися Сфотографувати Додати опитування Додати медіа Відкрити в браузері Медіа - Запити на стеження + Запити на підписку Заблоковані користувачі Закладки Вподобане Профіль Закрити Повторити - Видалити та перестворити + Видалити та переробити Видалити Змінити Поскаржитися @@ -86,7 +86,7 @@ Підписатися Ви впевнені, що хочете вийти %1$s\? Це призведе до видалення всіх локальних даних облікового запису, включно з чернетками та вподобаннями. Написати - Не подобається + Прибрати з обраного Додати в закладки Вподобати Відповісти @@ -98,14 +98,14 @@ Тут нічого немає. Потягніть вниз, щоб оновити! Нічого немає. Згорнути - Розгорнути + Показати більше Натисніть для перегляду Медіа приховано Попередження про вміст Змінити Написати - Не ховати розмову - Сховати розмову + Не ігнорувати розмову + Ігнорувати розмову Заплановані дописи Підписники Написати @@ -114,11 +114,11 @@ Сповіщення Заблоковані користувачі Вподобане - Запити на стеження - Не ховати %1$s - Не ховати - Сховані домени - Приховувані користувачі + Запити на підписку + Не ігнорувати %1$s + Не ігнорувати + Приховані домени + Ігноровані користувачі ПОШИРИТИ! ПОШИРИТИ Показати поширення @@ -129,24 +129,24 @@ %1$s вподобує ваш допис %1$s поширює ваш допис Згорнути - Розгорнути + Ще Делікатний вміст %1$s поширює - Сховані домени - Приховувані користувачі + Приховані домени + Ігноровані користувачі Дописи - Стрічка + Обговорення Вкладки Не вдалося відвантажити. Не вдалося отримати токен входу. Якщо проблема не зникає, спробуйте увійти через браузер з меню. Авторизацію відхилено. Якщо ви впевнені, що вказали правильні облікові дані, спробуйте увійти через браузер з меню. - Сталася помилка невпізнання авторизації. Якщо проблема не зникає, спробуйте увійти через браузер з меню. + Виникла невизначена помилка авторизації. Якщо вона не зникає, спробуйте увійти через браузер з меню. Помилка автентифікації цього сервера. Якщо проблема не зникає, спробуйте увійти через браузер з меню. Введено недійсний домен Показати поширення Показати поширення Головна стрічка - Не ховати %1$s + Не ігнорувати %1$s Видимість дописів Деякі відомості, які можуть вплинути на ваше психічний стан, буде приховано. Це включає: \n @@ -176,7 +176,7 @@ Стиль емодзі Ваш сервер %1$s не має власних емодзі Зберегти чернетку\? - Вимагає затвердження підписників власноруч + Доведеться затверджувати підписників власноруч Додати підпис Опис матеріалів для людей з вадами зору (обмеження %1$d символ) @@ -268,10 +268,10 @@ Надсилання дописів Помилка надсилання допису Надсилання допису… - Публікування як %1$s + Поширити як %1$s Вилучити обліковий запис зі списку Додати обліковий запис до списку - Пошук серед тих, на кого ви підписані + Пошук серед людей на яких ви підписані Видалити список Оновити список Створити список @@ -282,7 +282,7 @@ Додати обліковий запис Фільтрувати фразу Коли ключове слово або фраза є лише буквено-цифровими, вони застосовуватимуться лише, якщо вони збігатимуться з цілим словом - Ціле слово + Цілим словом Оновити Заблокувати обліковий запис Вилучити @@ -290,7 +290,7 @@ Редагувати фільтр Додати фільтр Розмови - Загальнодоступні стрічки + Публічні стрічки Завантажити ще Відповідь для @%1$s Завжди розгортати допис, з попередженнями про вміст @@ -349,7 +349,7 @@ Великий Середній Маленький - Найменший + Крихітний Розмір шрифту допису Лише для підписників Приховано @@ -361,7 +361,7 @@ Розташування головної панелі переходів Не вдалося синхронізувати налаштування Усталені налаштування дописів - Завжди позначати дописи делікатними (синхронізовано з сервером) + Завжди позначати медіа як чутливі (синхронізовано з сервером) Приватність дописів (синхронізовано з сервером) Порт HTTP-проксі Сервер HTTP-проксі @@ -392,12 +392,12 @@ Вібросповіщення Звукові сповіщення Попередження - Безпосередньо: Опублікувати лише для згаданих користувачів - Лише підписники: Опублікувати лише для підписників + Особисте згадування: Поширити лише для згаданих користувачів + Лише підписники: Допис лише для підписників Приховано: Не показувати у загальних стрічках Публічно: Опублікувати у загальних стрічках Сховати сповіщення - Сховати @%1$s? + Ігнорувати @%1$s? Заблокувати @%1$s? Сховати весь домен Ви впевнені, що хочете заблокувати все з %1$s? Ви не побачите матеріали з цього домену в загальнодоступних стрічках або у своїх сповіщеннях. Ваших підписників з цього домену буде вилучено. @@ -421,8 +421,8 @@ Аватар Відповісти… Показуване ім\'я - %1$s показано - Приховування користувача скасовано + %1$s більше не приховується + Ігнорування користувача скасовано Користувача розблоковано Поділитися медіа з… Поділитися дописом з… @@ -430,13 +430,13 @@ Завантаження медіа Завантажити медіа Відкрити медіа #%1$d - Хештеги - Хештеги - Хештеги + Гештеги + Гештеги + Гештеги Відкрити автора поширення Додати вкладку Запланувати допис - Клавіотура емодзі + Клавіатура емодзі Допис, для якого ви створили чернетку відповіді, вилучено Чернетку видалено Не вдалося завантажити дані відповіді @@ -464,8 +464,8 @@ Помилка пошуку допису %1$s Увімкнути перемикання між вкладками жестом проведення пальцем Не вдалося здійснити пошук - Обліковий запис з іншого сервера. Надіслати анонімізовану копію звіту й туди\? - Скаргу буде надіслано модераторам вашого сервера. Ви можете пояснити причину скарги на цей обліковий запис нижче: + Обліковий запис з іншого сервера. Надіслати туди анонімізовану копію звіту також? + Скаргу буде надіслано модераторам вашого сервера. Нижче ви можете надати пояснення, чому ви повідомляєте про цей обліковий запис: Не вдалося отримати дописи Переслати до %1$s Дії для зображення %1$s @@ -476,8 +476,8 @@ Видалити Список Вибрати список - Хештег без # - Додати хештег + Гештег без # + Додати гештег Назва списку Опитування з варіантами: %1$s, %2$s, %3$s, %4$s; %5$s Особисто @@ -491,20 +491,20 @@ Прикріпити Відкріпити - Наведені далі відомості можуть відбивати не повний профіль користувача. Натисніть, щоб відкрити повний профіль у браузері. - Показ абсолютного часу - Ярлик + Наведена нижче інформація може відображати профіль користувача не повністю. Натисніть, щоб відкрити повний профіль у браузері. + Використовуйте точний час + Мітка додати дані Метадані профілю - Ліцензовано ліцензією Apache (копія знизу) + Ліцензовано за ліцензією Apache (копія знизу) Tusky містить код та засоби з таких проєктів з відкритим кодом: Скасувати поширення Поширити початковій аудиторії - %1$s переміщено до: + %1$s переїхав до: Не вдалося завантажити Поточний набір емодзі Google Стандартний набір емодзі Mastodon - Навіть попри те, що ваш обліковий запис загальнодоступний, співробітники %1$s вважають, що ви, можливо, захочете переглянути запити від цих облікових записів власноруч. + Незважаючи на те, що ваш обліковий запис не заблоковано, співробітники %1$s вважають, що ви, можливо, захочете переглянути запити на підписку з цих акаунтів власноруч. Видалити цю бесіду\? Видалити бесіду Вилучити закладку @@ -527,7 +527,7 @@ Збереження чернетки… Відхилити Подробиці - Приєднується %1$s + Приєднався %1$s Редагувати зображення 1+ Неможливо редагувати зображення. @@ -541,7 +541,7 @@ Налаштувати точку фокусування Завжди Торкніться або перетягніть коло, щоб вибрати точку фокусування, яку завжди буде видно на мініатюрах. - Якщо ви ввійшли у кілька облікових записів + Коли ви ввійшли у кілька облікових записів Ніколи Показувати ім\'я користувача на панелях інструментів Видалити цей запланований допис\? @@ -566,14 +566,14 @@ У вас немає списків. Спам Інше - Цей сервер не підтримує слідкування за хештегами. - Відстежувані хештеги + Цей сервер не підтримує наступні гештеги. + Відстежувані гештеги Мова дописів (синхронізовано з сервером) %1$s (%2$s) Змінено %1$s Змінено - Помилка приховування #%1$s - Помилка увімкнення сповіщень #%1$s + Не вдалось ігнорувати #%1$s + Помилка припинення ігнорування #%1$s Медіа повинні мати опис. Порт повинен бути між %1$d і %2$d Не вдалося завантажити джерело стану з сервера. @@ -585,7 +585,7 @@ Редагування %1$s редагує %1$s створює - Завантаження стрічки + Обговорення завантажується Поділитися посиланням на обліковий запис Поділитися іменем користувача облікового запису Поділитися URL облікового запису через… @@ -617,11 +617,11 @@ \n \nАбо не вдалося зв\'язатися з сервером, або він відхилив дописи. - %1$d людей обговорюють хештеґ %2$s - Популярні хештеги + %1$d людей обговорюють гештеґ %2$s + Популярні гештеги Усього використано Усього облікових записів - Стежити за хештегом + Стежити за гештегом Оновити Невідомо Надто тривала спроба з\'єднання з вашим сервером @@ -631,10 +631,10 @@ Не вдалося вподобати допис: %1$s Не вдалося поширити допис: %1$s Не вдалося проголосувати: %1$s - Не вдалося погодити запит на стеження: %1$s - Не вдалося відхилити запит на стеження: %1$s - Запит на стеження погоджено - Запит на стеження заблоковано + Не вдалося погодити запит на підписку: %1$s + Не вдалося відхилити запит на підписку: %1$s + Запит на підписку погоджено + Запит на підписку заблоковано Усе одно показати Відфільтровано: %1$s Профілі @@ -659,7 +659,7 @@ Зображення Керувати списками Розмір шрифту інтерфейсу - Фонова діяльність + Фонова праця Сповіщення, коли Tusky працює у фоновому режимі Отримання сповіщень… Обслуговування кешу… @@ -681,7 +681,7 @@ Сховати з домашньої стрічки Тут розміщено ваші приватні повідомлення; іноді їх називають розмовами або прямими повідомленнями (DM). \n -\nПриватні повідомлення створюються шляхом налаштування видимості {{public}} допису на {{mail}} Особистий і згадування в тексті одного або кількох користувачів. +\nОсобисті повідомлення створюються шляхом налаштування видимості {{public}} допису на {{mail}} Особистий і згадування в тексті одного або кількох користувачів. \n \nНаприклад, ви можете почати з перегляду профілю облікового запису, натиснути кнопку створення [iconics gmd_edit] і змінити видимість. Не вдалося відтворити: %1$s @@ -693,18 +693,18 @@ \nЗАУВАЖТЕ, що ви можете додавати до своїх списків лише ті облікові записи, за якими слідкуєте. \n \nЦі списки можна використовувати як вкладку у налаштуваннях Вкладок облікового запису [iconics gmd_account_circle] [iconics gmd_navigate_next]. - Сховати хештег #%1$s як попередження - Показувати хештег #%1$s + Сховати гештег #%1$s як попередження + Показувати гештег #%1$s Переглянути фільтр - Хештег #%1$s відстежується - Хештег #%1$s більше не відстежується - Не вдається сховати %1$s: %2$s - Не вдалося показати %1$s: %2$s + Гештег #%1$s відстежується + Гештег #%1$s більше не відстежується + Не вдається ігнорувати %1$s: %2$s + Не вдалося припинити ігнорування %1$s: %2$s Зображення Тема системи (чорна) Популярні дописи - Показати самоцитування - Хтось цитує власний допис + Показати самопоширення + Хтось поширив власний допис Ніхто Учасники списку Будь-який користувач, за яким ви стежите @@ -718,13 +718,13 @@ Перекладається… Перекладено з %1$s на %2$s Неможливо перекласти: %1$s - Правові суперечності + Юридичні порушення Невідомий тип сповіщення Url скопійовано \'#%1$s\' скопійовано Стежити за обліковим записом? - Запит підтвердження перед початком стеження - Стежити за новим хештегом + Запитувати підтвердження перед підпискою на користувача + Стежити за новим гештегом Приватність відповіді (не синхронізовано з сервером) Помилка видалення фільтра \'%1$s\' Помилка збереження фільтра \'%1$s\' @@ -732,7 +732,7 @@ Попередньо вибрана приватність буде залежати від допису, на який ви відповідаєте. Особисті Завершується після - Відстежуваний хештег %1$s + Відстежуваний гештег %1$s Більше від %1$s Фільтровані сповіщення Сповіщення від %1$d людей, яких ви можете знати @@ -746,7 +746,7 @@ Сповіщення від %1$s Прийняти Відхилити - Відфільтровано, якщо це не відповідь на вашу власну згадку або якщо ви слідкуєте за відправником + Відфільтровано, якщо це не відповідь на вашу власну згадку або якщо ви підписані на відправника Від %1$s Відповідь У відповідь до %1$s @@ -756,18 +756,18 @@ Створені за попередні 30 днів Небажані приватні згадки Люди, за якими ви не слідкуєте - Доки ви не затвердите їх вручну - Люди, які не слідкують за вами - Включно з людьми, які слідкують за вами менше ніж 3 дні + Доки ви не затвердите їх власноруч + Люди, що не підписані на вас + Включно з людьми, що підписані на вас менше ніж 3 дні Нові облікові записи - Модерований обліковий запис + Облікові записи під керівництвом модератора Обмежено модераторами сервера Ця функція підтримується лише на серверах Mastodon версії 4.3.0 або новіших. Поширення для всіх Розірвані з\'єднання Невідомий тип сповіщення Адміністративні сповіщення, наприклад, скарги або реєстрації - З\'єднання розірвано + Зв\'язок втрачено Опитування Згадка Поширення лише для підписників @@ -783,4 +783,36 @@ Поширити без сповіщення інших Показати результати Адміністратор з %1$s обмежив %2$s, що означає, що ви більше не можете отримувати від них оновлення або взаємодіяти з ними. + Не ігнорується %1$s + Не вдалося почати ігнорувати %1$s: %2$s + Не вдалося розблокувати %1$s: %2$s + Не вдалося припини ігнорування %1$s: %2$s + Не вдалося відхилити запит підписки від %1$s: %2$s + Не вдалося заблокувати %1$s: %2$s + Не вдалося прийняти запит підписки від %1$s: %2$s + Ви заблокували %1$s, видаливши %2$s своїх підписників і%3$s , на яких ви підписані. + Деякі з ваших дописів були видалені. + Відтепер ваші дописи будуть позначені як чутливі. + Tusky не розпізнає цей тип сповіщень і не може показати його вам. Можливо, веб-інтерфейс вашого сервера зможе це зробити. Будь ласка, також повідомте про це розробників Tusky, щоб вони могли додати підтримку цього типу! + Розблоковано %1$s + Ви отримали попередження від модераторів. + + %1$s обліковий запис + %1$s облікових записа + %1$s облікових записів + %1$s облікових записів + + Ваш обліковий запис вимкнено. + Деякі з ваших дописів були позначені як чутливі. + Ваш акаунт отримав попередження від модераторів. + Адміністратор із %1$s заблокував %2$s, включно з %3$s ваших підписників і %4$s , за якими ви стежите. + + %1$s медіявкладень + %1$s медіявкладення + %1$s медіявкладень + %1$s медіявкладень + + Так + Ні + Відкинути зміну підписання? \ No newline at end of file From 7a2aca6b71c9033b1c822371b06a0b4fee5ca338 Mon Sep 17 00:00:00 2001 From: Rhoslyn Prys Date: Tue, 22 Apr 2025 02:07:23 +0000 Subject: [PATCH 31/54] Translated using Weblate (Welsh) Currently translated at 100.0% (709 of 709 strings) Co-authored-by: Rhoslyn Prys Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/cy/ Translation: Tusky/Tusky --- app/src/main/res/values-cy/strings.xml | 50 ++++++++++++++++---------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/app/src/main/res/values-cy/strings.xml b/app/src/main/res/values-cy/strings.xml index 866d8d9e2..1fb4c7b7a 100644 --- a/app/src/main/res/values-cy/strings.xml +++ b/app/src/main/res/values-cy/strings.xml @@ -114,12 +114,12 @@ Pennyn Beth yw gweinydd\? Yn cysylltu… - Gallwch chi roi cyfeiriad neu barth o unrhyw weinydd yma, fel mastodon.social, twt.cymru, social.tchncs.de, a mwy! -\n -\nOs nad oes gennych chi gyfrif, gallwch chi roi enw\'r gweinydd yr hoffech chi ymuno ag ef a chreu cyfrif yno. -\n -\nGweinydd yw\'r man y mae\'ch gyfrif wedi\'i gynnal, ond gallwch chi gyfathrebu\'n hawdd â phobl a\'u dilyn ar weinyddion eraill fel petaech yn yr unfan. -\n + Gallwch chi roi cyfeiriad neu barth o unrhyw weinydd yma, fel mastodon.social, twt.cymru, social.tchncs.de, a rhagor! +\n +\nOs nad oes gennych chi gyfrif, gallwch chi roi enw\'r gweinydd yr hoffech chi ymuno ag ef a chreu cyfrif yno. +\n +\nGweinydd yw\'r man y mae\'ch gyfrif wedi\'i gynnal, ond gallwch chi gyfathrebu\'n hawdd â phobl a\'u dilyn ar weinyddion eraill fel petaech yn yr unfan. +\n \nRhagor o wybodaeth yn joinmastodon.org. Yn Gorffen Llwytho\'r Cyfryngau i Fyny Yn llwytho i fyny… @@ -689,8 +689,8 @@ %1$s: %2$s Delwedd Rheoli rhestrau - Dyma\'ch llinell amser cartref. Mae\'n dangos negeseuon diweddar y cyfrifon rydych chi\'n eu dilyn. -\n + Dyma\'ch llinell amser cartref. Mae\'n dangos negeseuon diweddar y cyfrifon rydych chi\'n eu dilyn. +\n \nEr mwyn archwilio cyfrifon gallwch ddod o hyd iddyn nhw o fewn un o\'r llinellau amser eraill. Er enghraifft, llinellau amser eich gweinydd {{group}}. Neu gallwch eu chwilio yn ôl eu henw {{search}}; er enghraifft, chwiliwch am Tusky i ddod o hyd i\'n cyfrif Mastodon. Dangos ystadegau negeseuon yn y ffrwd Maint testun rhyngwyneb @@ -698,8 +698,8 @@ Hysbysiadau pan fydd Tusky\'n gweithio\'n y cefndir Yn estyn hysbysiadau… Cynnal a chadw\'r cof dros dro… - Mae eich gweinydd yn gwybod y cafodd y neges hon ei golygu, ond nid oes ganddo gopi o\'r golygiadau, felly nid oes modd eu dangos i chi. -\n + Mae eich gweinydd yn gwybod y cafodd y neges hon ei golygu, ond nid oes ganddo gopi o\'r golygiadau, felly nid oes modd eu dangos i chi. +\n \nDyma broblem Mastodon #25398. Llwytho hysbysiadau diweddaraf Dileu\'r drafft\? @@ -720,10 +720,10 @@ Hoffech chi gadw\'r newidiadau i\'ch proffil\? Delwedd Defnyddio Cynllun y System (du) - Dyma\'ch negeseuon preifat; weithiau\'n cael eu galw\'n sgyrsiau neu negeseuon uniongyrchol. -\n -\nMae negeseuon preifat yn cael eu creu drwy osod y gwelededd {{public}} neges i {{mail}} Uniongyrchol a chyfeirio at un neu ragor o ddefnyddwyr yn y testun. -\n + Dyma\'ch negeseuon preifat; weithiau\'n cael eu galw\'n sgyrsiau neu negeseuon uniongyrchol (DM). +\n +\nMae negeseuon preifat yn cael eu creu drwy osod y gwelededd {{public}} neges i {{mail}} Uniongyrchol a chyfeirio at un neu ragor o ddefnyddwyr yn y testun. +\n \nEr enghraifft gallwch chi dechrau ar y proffil golwg cyfrif a phwyso\'r botwm creu [iconics gmd_edit] a newid y gwelededd. Yn dad-dewi hashnod #%1$s Yn tewi hashnod #%1$s fel rhybudd @@ -733,10 +733,10 @@ Nawr yn dilyn yr hashnod #%1$s Wedi methu dad-dewi %1$s: %2$s Wedi methu tewi %1$s: %2$s - Dyma\'ch golwg rhestrau. Gallwch chi diffinio nifer o restrau preifat ac yn ychwanegu cyfrifon atyn nhw. -\n -\nSYLWCH y gallwch chi ychwanegu dim ond cyfrifon rydych chi\'n eu dilyn i\'ch rhestrau. -\n + Dyma\'ch golwg rhestrau. Gallwch chi diffinio nifer o restrau preifat ac yn ychwanegu cyfrifon atyn nhw. +\n +\nSYLWCH y gallwch chi ychwanegu dim ond cyfrifon rydych chi\'n eu dilyn i\'ch rhestrau. +\n \nGall y rhestrau hyn yn cael eu defnyddio fel tab yn newisiadau Tabiau Cyfrif [iconics gmd_account_circle] [iconics gmd_navigate_next]. Aelodau\'r rhestr Unrhyw ddefnyddiwr a ddilynir @@ -842,4 +842,16 @@ Rydych wedi derbyn rhybudd gan gymedrolwr. Mae rhai o\'ch negeseuon wedi\'u dileu. Nid yw Tusky yn adnabod y math hwn o hysbysiad ac ni all ei ddangos i chi.Rhowch wybod hefyd i ddatblygwyr Tusky fel y gallant ychwanegu cefnogaeth ar gyfer y math hwn! - + Wedi dadrwystro %1$s + Wedi dad-dewi %1$s + Wedi methu rhwystro %1$s: %2$s + Wedi methu tewi %1$s: %2$s + Wedi methu dadrwystro %1$s: %2$s + Wedi methu dad-dewi %1$s: %2$s + Wedi methu derbyn cais i\'ch dilyn gan %1$s: %2$s + Wedi methu gwrthod cais i\'ch dilyn gan %1$s: %2$s + Dangos canlyniadau + Iawn + Na + Dileu newidiadau capsiynau? + \ No newline at end of file From bf065e5d8996e35ff787ae038265f969a00959c4 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Tue, 22 Apr 2025 02:07:23 +0000 Subject: [PATCH 32/54] Translated using Weblate (Ukrainian) Currently translated at 100.0% (709 of 709 strings) Co-authored-by: Ihor Hordiichuk Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/uk/ Translation: Tusky/Tusky --- app/src/main/res/values-uk/strings.xml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index c64588e37..5d00e5369 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -465,7 +465,7 @@ Увімкнути перемикання між вкладками жестом проведення пальцем Не вдалося здійснити пошук Обліковий запис з іншого сервера. Надіслати туди анонімізовану копію звіту також? - Скаргу буде надіслано модераторам вашого сервера. Нижче ви можете надати пояснення, чому ви повідомляєте про цей обліковий запис: + Скаргу буде надіслано модераторам вашого сервера. Нижче ви можете описати причину скарги на цей обліковий запис: Не вдалося отримати дописи Переслати до %1$s Дії для зображення %1$s @@ -504,7 +504,7 @@ Не вдалося завантажити Поточний набір емодзі Google Стандартний набір емодзі Mastodon - Незважаючи на те, що ваш обліковий запис не заблоковано, співробітники %1$s вважають, що ви, можливо, захочете переглянути запити на підписку з цих акаунтів власноруч. + Попри те, що ваш обліковий запис не заблоковано, співробітники %1$s вважають, що ви, можливо, захочете розглянути запити на підписку від цих облікових записів власноруч. Видалити цю бесіду\? Видалити бесіду Вилучити закладку @@ -527,7 +527,7 @@ Збереження чернетки… Відхилити Подробиці - Приєднався %1$s + Приєднується %1$s Редагувати зображення 1+ Неможливо редагувати зображення. @@ -659,7 +659,7 @@ Зображення Керувати списками Розмір шрифту інтерфейсу - Фонова праця + Фонова діяльність Сповіщення, коли Tusky працює у фоновому режимі Отримання сповіщень… Обслуговування кешу… @@ -767,7 +767,7 @@ Розірвані з\'єднання Невідомий тип сповіщення Адміністративні сповіщення, наприклад, скарги або реєстрації - Зв\'язок втрачено + З\'єднання розірвано Опитування Згадка Поширення лише для підписників @@ -792,8 +792,8 @@ Не вдалося прийняти запит підписки від %1$s: %2$s Ви заблокували %1$s, видаливши %2$s своїх підписників і%3$s , на яких ви підписані. Деякі з ваших дописів були видалені. - Відтепер ваші дописи будуть позначені як чутливі. - Tusky не розпізнає цей тип сповіщень і не може показати його вам. Можливо, веб-інтерфейс вашого сервера зможе це зробити. Будь ласка, також повідомте про це розробників Tusky, щоб вони могли додати підтримку цього типу! + Відтепер ваші дописи будуть позначені делікатними. + Tusky не розпізнає цей тип сповіщень і не може показати його вам. Можливо, вебінтерфейс вашого сервера зможе це зробити. Будь ласка, також повідомте про це розробників Tusky, щоб вони могли додати підтримку цього типу! Розблоковано %1$s Ви отримали попередження від модераторів. @@ -803,14 +803,14 @@ %1$s облікових записів Ваш обліковий запис вимкнено. - Деякі з ваших дописів були позначені як чутливі. - Ваш акаунт отримав попередження від модераторів. + Деякі з ваших дописів були позначені делікатними. + Ваш обліковий запис отримав попередження від модераторів. Адміністратор із %1$s заблокував %2$s, включно з %3$s ваших підписників і %4$s , за якими ви стежите. - %1$s медіявкладень - %1$s медіявкладення - %1$s медіявкладень - %1$s медіявкладень + %1$s медіавкладення + %1$s медіавкладення + %1$s медіавкладень + %1$s медіавкладень Так Ні From 83969b2b224d8dd1f7d1cda6274a6a92ddfd7d69 Mon Sep 17 00:00:00 2001 From: mittwerk Date: Tue, 22 Apr 2025 16:07:21 +0300 Subject: [PATCH 33/54] translation to 36 api (#5071) --- app/build.gradle | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 4615ded61..24d7ac087 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -25,13 +25,13 @@ final def CUSTOM_INSTANCE = "" final def SUPPORT_ACCOUNT_URL = "https://mastodon.social/@Tusky" android { - compileSdk 35 + compileSdk 36 namespace "com.keylesspalace.tusky" defaultConfig { applicationId APP_ID namespace "com.keylesspalace.tusky" minSdk 24 - targetSdk 35 + targetSdk 36 versionCode 131 versionName "28.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -103,11 +103,11 @@ android { // Exclude unneeded files added by libraries packagingOptions.resources.excludes += [ - 'LICENSE_OFL', - 'LICENSE_UNICODE', - 'META-INF/androidx/**', - 'META-INF/NOTICE.md', - 'DebugProbesKt.bin' + 'LICENSE_OFL', + 'LICENSE_UNICODE', + 'META-INF/androidx/**', + 'META-INF/NOTICE.md', + 'DebugProbesKt.bin' ] bundle { From c4df414735fdebd8eda71dda9462ca2e87c20dc2 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Tue, 22 Apr 2025 15:10:21 +0200 Subject: [PATCH 34/54] improve rendering quality when zooming into large images (#5068) Glide downscales images to fit ImageViews, which makes perfect sense, except here where the ImageView is zoomable. Always loading the memory in full resolution of course uses more memory but I think this is worth the additional quality. If it leds to performance problems or crashes I will check if we can mitigate those in another way. Before / after: ![Screenshot_20250421_111204](https://github.com/user-attachments/assets/f00e9e54-d78b-40af-b2fc-0cfb83e588a6) ![Screenshot_20250421_110843](https://github.com/user-attachments/assets/b8319582-d149-4e47-ac08-7eb8a46be208) --- .../java/com/keylesspalace/tusky/fragment/ViewImageFragment.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewImageFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewImageFragment.kt index 6faf739c2..8e8df5893 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewImageFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewImageFragment.kt @@ -282,9 +282,11 @@ class ViewImageFragment : ViewMediaFragment() { // Request image from the network on fail load image from cache .error( glide.load(url) + .override(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) .centerInside() .addListener(ImageRequestListener(false, isThumbnailRequest = false)) ) + .override(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) .centerInside() .addListener(ImageRequestListener(true, isThumbnailRequest = false)) .into(photoView) From 9441d004c5918ef6d30fcaa8e764eb2fd35268a7 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Tue, 22 Apr 2025 15:10:45 +0200 Subject: [PATCH 35/54] fix AboutActivity layout while loading instance data (#5066) closes #5062 --- app/src/main/res/layout/activity_about.xml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/layout/activity_about.xml b/app/src/main/res/layout/activity_about.xml index e891a4a12..3646ae39a 100644 --- a/app/src/main/res/layout/activity_about.xml +++ b/app/src/main/res/layout/activity_about.xml @@ -130,23 +130,25 @@ android:id="@+id/aboutPoweredByTusky" android:layout_width="0dp" android:layout_height="wrap_content" + android:layout_marginHorizontal="@dimen/text_content_margin" android:layout_marginTop="16dp" android:text="@string/about_powered_by_tusky" android:textAppearance="@style/TextAppearance.AppCompat.Subhead" - app:layout_constraintEnd_toEndOf="@+id/copyDeviceInfo" - app:layout_constraintStart_toStartOf="@+id/deviceInfo" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/accountInfo" /> From dea433f07948f007bb347edd39343a047dc9cd3a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 22 Apr 2025 15:49:08 +0200 Subject: [PATCH 36/54] Update emoji2 to v1.5.0 (#4656) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [androidx.emoji2:emoji2-views-helper](https://developer.android.com/jetpack/androidx/releases/emoji2#1.5.0) ([source](https://cs.android.com/androidx/platform/frameworks/support)) | `1.4.0` -> `1.5.0` | [![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.emoji2:emoji2-views-helper/1.5.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.emoji2:emoji2-views-helper/1.5.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.emoji2:emoji2-views-helper/1.4.0/1.5.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.emoji2:emoji2-views-helper/1.4.0/1.5.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [androidx.emoji2:emoji2-views](https://developer.android.com/jetpack/androidx/releases/emoji2#1.5.0) ([source](https://cs.android.com/androidx/platform/frameworks/support)) | `1.4.0` -> `1.5.0` | [![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.emoji2:emoji2-views/1.5.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.emoji2:emoji2-views/1.5.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.emoji2:emoji2-views/1.4.0/1.5.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.emoji2:emoji2-views/1.4.0/1.5.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [androidx.emoji2:emoji2](https://developer.android.com/jetpack/androidx/releases/emoji2#1.5.0) ([source](https://cs.android.com/androidx/platform/frameworks/support)) | `1.4.0` -> `1.5.0` | [![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.emoji2:emoji2/1.5.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.emoji2:emoji2/1.5.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.emoji2:emoji2/1.4.0/1.5.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.emoji2:emoji2/1.4.0/1.5.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- > [!WARNING] > Some dependencies could not be looked up. Check the Dependency Dashboard for more information. --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about these updates again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/tuskyapp/Tusky). --- gradle/libs.versions.toml | 2 +- gradle/verification-metadata.xml | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 29d5985bf..c83b99f55 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -27,7 +27,7 @@ bouncycastle = "1.70" conscrypt = "2.5.3" coroutines = "1.10.2" diffx = "1.1.1" -emoji2 = "1.4.0" +emoji2 = "1.5.0" filemoji-compat = "3.2.7" glide = "4.16.0" glide-animation-plugin = "3.0.4" diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 00d77dee5..f4a5d8c22 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -1062,6 +1062,14 @@ + + + + + + + + @@ -1073,6 +1081,14 @@ + + + + + + + + @@ -1084,6 +1100,14 @@ + + + + + + + + From 128fd4f573bf4faca0f07d27571477b5b9b49617 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 25 Apr 2025 10:59:10 +0200 Subject: [PATCH 37/54] Update dependency androidx.exifinterface:exifinterface to v1.4.1 (#5079) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [androidx.exifinterface:exifinterface](https://developer.android.com/jetpack/androidx/releases/exifinterface#1.4.1) ([source](https://cs.android.com/androidx/platform/frameworks/support)) | `1.4.0` -> `1.4.1` | [![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.exifinterface:exifinterface/1.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.exifinterface:exifinterface/1.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.exifinterface:exifinterface/1.4.0/1.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.exifinterface:exifinterface/1.4.0/1.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/tuskyapp/Tusky). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- gradle/verification-metadata.xml | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c83b99f55..a3613f9af 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,7 +7,7 @@ androidx-cardview = "1.0.0" androidx-constraintlayout = "2.2.1" androidx-core = "1.16.0" androidx-drawerlayout = "1.2.0" -androidx-exifinterface = "1.4.0" +androidx-exifinterface = "1.4.1" androidx-fragment = "1.8.6" androidx-hilt = "1.2.0" androidx-junit = "1.2.1" diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index f4a5d8c22..d6706fdcf 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -1127,6 +1127,14 @@ + + + + + + + + From 1971f8c686b81343fa6998996b4f68949715c2cd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 25 Apr 2025 10:59:18 +0200 Subject: [PATCH 38/54] Update dependency androidx.core:core-splashscreen to v1.2.0-beta02 (#5078) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [androidx.core:core-splashscreen](https://developer.android.com/jetpack/androidx/releases/core#1.0.1) ([source](https://cs.android.com/androidx/platform/frameworks/support)) | `1.2.0-beta01` -> `1.2.0-beta02` | [![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.core:core-splashscreen/1.2.0-beta02?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.core:core-splashscreen/1.2.0-beta02?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.core:core-splashscreen/1.2.0-beta01/1.2.0-beta02?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.core:core-splashscreen/1.2.0-beta01/1.2.0-beta02?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/tuskyapp/Tusky). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- gradle/verification-metadata.xml | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a3613f9af..c67716e0a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,7 @@ androidx-paging = "3.3.6" androidx-preference = "1.2.1" androidx-recyclerview = "1.4.0" androidx-sharetarget = "1.2.0" -androidx-splashscreen = "1.2.0-beta01" +androidx-splashscreen = "1.2.0-beta02" androidx-swiperefresh-layout = "1.1.0" androidx-testing = "2.2.0" androidx-viewpager2 = "1.1.0" diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index d6706fdcf..2912c36a5 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -603,6 +603,14 @@ + + + + + + + + From 1d11c79312f2d7edef67acb96155d80203effb7e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 25 Apr 2025 10:59:26 +0200 Subject: [PATCH 39/54] Update androidx.work to v2.10.1 (#5076) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [androidx.work:work-testing](https://developer.android.com/jetpack/androidx/releases/work#2.10.1) ([source](https://cs.android.com/androidx/platform/frameworks/support)) | `2.10.0` -> `2.10.1` | [![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.work:work-testing/2.10.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.work:work-testing/2.10.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.work:work-testing/2.10.0/2.10.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.work:work-testing/2.10.0/2.10.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [androidx.work:work-runtime-ktx](https://developer.android.com/jetpack/androidx/releases/work#2.10.1) ([source](https://cs.android.com/androidx/platform/frameworks/support)) | `2.10.0` -> `2.10.1` | [![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.work:work-runtime-ktx/2.10.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.work:work-runtime-ktx/2.10.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.work:work-runtime-ktx/2.10.0/2.10.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.work:work-runtime-ktx/2.10.0/2.10.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about these updates again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/tuskyapp/Tusky). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- gradle/verification-metadata.xml | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c67716e0a..724a8b988 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,7 +21,7 @@ androidx-splashscreen = "1.2.0-beta02" androidx-swiperefresh-layout = "1.1.0" androidx-testing = "2.2.0" androidx-viewpager2 = "1.1.0" -androidx-work = "2.10.0" +androidx-work = "2.10.1" androidx-room = "2.7.0" bouncycastle = "1.70" conscrypt = "2.5.3" diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 2912c36a5..9bddc59a4 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -3836,6 +3836,14 @@ + + + + + + + + @@ -3860,6 +3868,14 @@ + + + + + + + + @@ -3887,6 +3903,14 @@ + + + + + + + + From 1dc66a4acff9eeef1bd032051508c8390b99cb7c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 26 Apr 2025 08:03:45 +0200 Subject: [PATCH 40/54] Update androidx.room to v2.7.1 (#5075) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [androidx.room:room-testing](https://developer.android.com/jetpack/androidx/releases/room#2.7.1) ([source](https://cs.android.com/androidx/platform/frameworks/support)) | `2.7.0` -> `2.7.1` | [![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.room:room-testing/2.7.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.room:room-testing/2.7.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.room:room-testing/2.7.0/2.7.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.room:room-testing/2.7.0/2.7.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [androidx.room:room-ktx](https://developer.android.com/jetpack/androidx/releases/room#2.7.1) ([source](https://cs.android.com/androidx/platform/frameworks/support)) | `2.7.0` -> `2.7.1` | [![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.room:room-ktx/2.7.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.room:room-ktx/2.7.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.room:room-ktx/2.7.0/2.7.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.room:room-ktx/2.7.0/2.7.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [androidx.room:room-paging](https://developer.android.com/jetpack/androidx/releases/room#2.7.1) ([source](https://cs.android.com/androidx/platform/frameworks/support)) | `2.7.0` -> `2.7.1` | [![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.room:room-paging/2.7.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.room:room-paging/2.7.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.room:room-paging/2.7.0/2.7.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.room:room-paging/2.7.0/2.7.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | | [androidx.room:room-compiler](https://developer.android.com/jetpack/androidx/releases/room#2.7.1) ([source](https://cs.android.com/androidx/platform/frameworks/support)) | `2.7.0` -> `2.7.1` | [![age](https://developer.mend.io/api/mc/badges/age/maven/androidx.room:room-compiler/2.7.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/androidx.room:room-compiler/2.7.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/androidx.room:room-compiler/2.7.0/2.7.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/androidx.room:room-compiler/2.7.0/2.7.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about these updates again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/tuskyapp/Tusky). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- gradle/verification-metadata.xml | 112 +++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 724a8b988..05263c111 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,7 +22,7 @@ androidx-swiperefresh-layout = "1.1.0" androidx-testing = "2.2.0" androidx-viewpager2 = "1.1.0" androidx-work = "2.10.1" -androidx-room = "2.7.0" +androidx-room = "2.7.1" bouncycastle = "1.70" conscrypt = "2.5.3" coroutines = "1.10.2" diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 9bddc59a4..593d7a31b 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -3277,6 +3277,14 @@ + + + + + + + + @@ -3285,6 +3293,14 @@ + + + + + + + + @@ -3301,6 +3317,14 @@ + + + + + + + + @@ -3317,6 +3341,14 @@ + + + + + + + + @@ -3325,6 +3357,14 @@ + + + + + + + + @@ -3344,6 +3384,14 @@ + + + + + + + + @@ -3360,6 +3408,14 @@ + + + + + + + + @@ -3368,6 +3424,14 @@ + + + + + + + + @@ -3387,6 +3451,14 @@ + + + + + + + + @@ -3395,6 +3467,14 @@ + + + + + + + + @@ -3416,6 +3496,14 @@ + + + + + + + + @@ -3424,6 +3512,14 @@ + + + + + + + + @@ -3443,6 +3539,14 @@ + + + + + + + + @@ -3451,6 +3555,14 @@ + + + + + + + + From ce8f6730e59424c6df831cd720d5a9fce1ed92c3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 26 Apr 2025 08:03:54 +0200 Subject: [PATCH 41/54] Update plugin com.gradle.develocity to v4.0.1 (#5080) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | com.gradle.develocity | `4.0` -> `4.0.1` | [![age](https://developer.mend.io/api/mc/badges/age/maven/com.gradle.develocity:com.gradle.develocity.gradle.plugin/4.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/maven/com.gradle.develocity:com.gradle.develocity.gradle.plugin/4.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/maven/com.gradle.develocity:com.gradle.develocity.gradle.plugin/4.0/4.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/maven/com.gradle.develocity:com.gradle.develocity.gradle.plugin/4.0/4.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/tuskyapp/Tusky). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/verification-metadata.xml | 13 +++++++++++++ settings.gradle | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 593d7a31b..ceff45874 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -14600,6 +14600,14 @@ + + + + + + + + @@ -14650,6 +14658,11 @@ + + + + + diff --git a/settings.gradle b/settings.gradle index 422643785..9218ec84e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -11,7 +11,7 @@ pluginManagement { } plugins { - id 'com.gradle.develocity' version '4.0' + id 'com.gradle.develocity' version '4.0.1' } def isCI = providers.environmentVariable("CI").present From 92a17a9a36229e4cefc575cbb3feb1d3bad62675 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 26 Apr 2025 08:04:08 +0200 Subject: [PATCH 42/54] Update dependency gradle to v8.14 (#5081) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Update | Change | |---|---|---| | [gradle](https://gradle.org) ([source](https://redirect.github.com/gradle/gradle)) | minor | `8.13` -> `8.14` | --- ### Release Notes
gradle/gradle (gradle) ### [`v8.14`](https://redirect.github.com/gradle/gradle/compare/v8.13.0...v8.14.0) [Compare Source](https://redirect.github.com/gradle/gradle/compare/v8.13.0...v8.14.0)
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/tuskyapp/Tusky). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/wrapper/gradle-wrapper.jar | Bin 43705 -> 43764 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 4 ++-- gradlew.bat | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 9bbc975c742b298b441bfb90dbc124400a3751b9..1b33c55baabb587c669f562ae36f953de2481846 100644 GIT binary patch delta 642 zcmdmamFde>rVZJA^}0Q$xegf!xPEW^+5YDM%iT2bEgct9o+jH~+sJas#HZ=szO|** z=Pj=X_vx?W&DSwKck|WWn~hffsvnQ+42*W$b7b0$SCcOoZ`{W{^$^pk;4>8-A*-)$ z?n(Po`1$6Jn_u?t-L+tsPyZ2#X}8T6OS8pAU;kdgd+_Hw4z4TW0p9E!T+=f7-c&O% zFic^X{7^$?^Ho04eona9n#mGMxKhA=~8B%JN`M zMhm5wc-2v)$``sY$!Q`9xiU@DhI73ZxiGEKg>yIPs)NmWwMdF-ngLXpZSqV5ez36n zVkxF2rjrjWR+_xr6e6@_u@s~2uv{9vi*1pj2)BjFD+-%@&pRVP1f{O1glxTOp2-62Ph;v z`N1+vCd)9ea)af*Ol1*JCfnp$%Uu}%OuoN7g2}3C@`L5FlP#(sA=|h@iixuZC?qp^ z=L$=v$ZoI}|87Wh=&h7udff{aieKr*l+zDp?pf)_bbRvUf>kn;HCDMXNlgbbo!QRK I1x7am0No)LiU0rr delta 584 zcmexzm1*ZyrVZJAexH5Moc8h7)w{^+t*dqJ%=yhh23L$9JpFV=_k`zJ-?Q4DI*eSe z+ES)HSrVnWLtJ&)lO%hRkV9zl5qqWRt0e;bb zPPo`)y?HTAyZI&u&X<|2$FDHCf4;!v8}p=?Tm`^F0`u(|1ttf~&t$qP3KUSD>@TJQ zRwJ}Pim6NzEc8KA6)e;S6gs8=7IIL8sQL*MYEuRYO;Uj<%3UbMbV&^&!Zvx+LKmjT z8Zch6rYP7Tw?$Hn(UTJwWiS=$f{lB(C=e*%usDV})0AQIK~sat=ND@+Gg*Pyij!rR z*fa02W|%BsV++>4W{DKDGSIUEHd2$P+8ct!RF+CHDowUuTEZOZ%rJSQv*qOXOSPDN zT|sP-$p*_3ncsWB*qoD7JQcyZ9xan%cJP6Tb4-?AZpr*F6v98hoNaPJm@HV`yya5N z))6pqFXn@}P(3T0nEzM8*c_9KtE9o|_pFd&K35GBXP^9Kg(b6GH-z8S4GDzIl~T+b zdLd#meKKHu$5u))8cu$=GKINkGDPOUD)!0$C(BH(U!}!-e;Q0ok8Sc?V1zRO04>ts AA^-pY diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 37f853b1c..ca025c83a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index faf93008b..23d15a936 100755 --- a/gradlew +++ b/gradlew @@ -114,7 +114,7 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar +CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. @@ -213,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 9d21a2183..db3a6ac20 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -70,11 +70,11 @@ goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar +set CLASSPATH= @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell From 071458708a19431e4be88709bcab6f1fd30d5ef7 Mon Sep 17 00:00:00 2001 From: Alexia Chaviara Date: Mon, 28 Apr 2025 23:01:05 +0300 Subject: [PATCH 43/54] 4986 - Add delete_media parameter support when deleting statuses (#5082) ### Summary This pull request updates the delete status API call to support the optional `delete_media` parameter introduced in Mastodon 4.2. ### Changes Updated `deleteStatus` function to accept a `delete_media` query parameter Fixes #4986 --- .../tusky/components/search/SearchViewModel.kt | 8 ++++---- .../search/fragments/SearchStatusesFragment.kt | 12 ++++++------ .../com/keylesspalace/tusky/fragment/SFragment.kt | 4 ++-- .../com/keylesspalace/tusky/network/MastodonApi.kt | 5 ++++- .../com/keylesspalace/tusky/usecase/TimelineCases.kt | 4 ++-- 5 files changed, 18 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt index 3114edc71..ebdd38e69 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt @@ -121,9 +121,9 @@ class SearchViewModel @Inject constructor( hashtagsPagingSourceFactory.newSearch(query) } - fun removeItem(statusViewData: StatusViewData.Concrete) { + fun removeItem(statusViewData: StatusViewData.Concrete, deleteMedia: Boolean) { viewModelScope.launch { - if (timelineCases.delete(statusViewData.id).isSuccess) { + if (timelineCases.delete(statusViewData.id, deleteMedia).isSuccess) { if (loadedStatuses.remove(statusViewData)) { statusesPagingSourceFactory.invalidate() } @@ -207,9 +207,9 @@ class SearchViewModel @Inject constructor( } } - fun deleteStatusAsync(id: String): Deferred> { + fun deleteStatusAsync(id: String, deleteMedia: Boolean): Deferred> { return viewModelScope.async { - timelineCases.delete(id) + timelineCases.delete(id, deleteMedia) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt index 887fc3283..c88c5b41d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt @@ -265,9 +265,9 @@ class SearchStatusesFragment : SearchFragment(), Status override fun clearWarningAction(position: Int) {} - private fun removeItem(position: Int) { + private fun removeItem(position: Int, deleteMedia: Boolean) { adapter?.peek(position)?.let { - viewModel.removeItem(it) + viewModel.removeItem(it, deleteMedia) } } @@ -578,8 +578,8 @@ class SearchStatusesFragment : SearchFragment(), Status MaterialAlertDialogBuilder(it) .setMessage(R.string.dialog_delete_post_warning) .setPositiveButton(android.R.string.ok) { _, _ -> - viewModel.deleteStatusAsync(id) - removeItem(position) + viewModel.deleteStatusAsync(id, true) + removeItem(position, true) } .setNegativeButton(android.R.string.cancel, null) .show() @@ -592,9 +592,9 @@ class SearchStatusesFragment : SearchFragment(), Status .setMessage(R.string.dialog_redraft_post_warning) .setPositiveButton(android.R.string.ok) { _, _ -> viewLifecycleOwner.lifecycleScope.launch { - viewModel.deleteStatusAsync(id).await().fold( + viewModel.deleteStatusAsync(id, false).await().fold( { deletedStatus -> - removeItem(position) + removeItem(position, false) val redraftStatus = if (deletedStatus.isEmpty) { status.toDeletedStatus() diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.kt index ca6ca4c89..0e2fa110e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.kt @@ -432,7 +432,7 @@ abstract class SFragment(@LayoutRes contentLayoutId: Int) : Fragment(contentLayo .setMessage(R.string.dialog_delete_post_warning) .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> viewLifecycleOwner.lifecycleScope.launch { - val result = timelineCases.delete(id).exceptionOrNull() + val result = timelineCases.delete(id, true).exceptionOrNull() if (result != null) { Log.w("SFragment", "error deleting status", result) Toast.makeText(requireContext(), R.string.error_generic, Toast.LENGTH_SHORT).show() @@ -456,7 +456,7 @@ abstract class SFragment(@LayoutRes contentLayoutId: Int) : Fragment(contentLayo .setMessage(R.string.dialog_redraft_post_warning) .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> viewLifecycleOwner.lifecycleScope.launch { - timelineCases.delete(id).fold( + timelineCases.delete(id, false).fold( { deletedStatus -> removeItem(position) val sourceStatus = if (deletedStatus.isEmpty) { 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 c329ea0fe..9b35c03dc 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -250,7 +250,10 @@ interface MastodonApi { ): Response> @DELETE("api/v1/statuses/{id}") - suspend fun deleteStatus(@Path("id") statusId: String): NetworkResult + suspend fun deleteStatus( + @Path("id") statusId: String, + @Query("delete_media") deleteMedia: Boolean? = null + ): NetworkResult @FormUrlEncoded @POST("api/v1/statuses/{id}/reblog") diff --git a/app/src/main/java/com/keylesspalace/tusky/usecase/TimelineCases.kt b/app/src/main/java/com/keylesspalace/tusky/usecase/TimelineCases.kt index 4fa2ed3c4..3d21318ab 100644 --- a/app/src/main/java/com/keylesspalace/tusky/usecase/TimelineCases.kt +++ b/app/src/main/java/com/keylesspalace/tusky/usecase/TimelineCases.kt @@ -109,8 +109,8 @@ class TimelineCases @Inject constructor( } } - suspend fun delete(statusId: String): NetworkResult { - return mastodonApi.deleteStatus(statusId) + suspend fun delete(statusId: String, deleteMedia: Boolean): NetworkResult { + return mastodonApi.deleteStatus(statusId, deleteMedia) .onSuccess { eventHub.dispatch(StatusDeletedEvent(statusId)) } .onFailure { Log.w(TAG, "Failed to delete status", it) } } From ab67f6cefe4d010f2cac8209389df2c70fba70e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sveinn=20=C3=AD=20Felli?= Date: Mon, 28 Apr 2025 17:07:14 +0000 Subject: [PATCH 44/54] Translated using Weblate (Icelandic) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (709 of 709 strings) Co-authored-by: Sveinn í Felli Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/is/ Translation: Tusky/Tusky --- app/src/main/res/values-is/strings.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index d4af50b5f..17ef4ff5e 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -667,11 +667,11 @@ \nEinkaskilaboð eru gerð með því að stilla sýnileika {{public}} færslu {{mail}} á Beint og að minnast á einn eða fleiri notendur í textanum. \n \nÞú getur til dæmis farið á notandaaðgang einhvers og ýtt á \'Minnast á\'-hnappinn [iconics gmd_edit] og breytt sýnileikanum.
- Þetta er listasýnin þín. Þú getur skilgreint fjölmarga einkalista og bætt notendaaðgöngum á þá. -\n -\nATHUGAÐU: þú getur bara bætt notendaaðgöngum sem þú fylgist með á listana þína. -\n -\nÞessa lista má nota sem flipa í Kjörstillingar aðgangs{{manage_accounts}} {{chevron_right} flipunum . + Þetta er listasýnin þín. Þú getur skilgreint fjölmarga einkalista og bætt notendaaðgöngum á þá. +\n +\nATHUGAÐU: þú getur bara bætt notendaaðgöngum sem þú fylgist með á listana þína. +\n +\nÞessa lista má nota sem flipa í Kjörstillingar aðgangs{{manage_accounts}} {{chevron_right} flipunum. Mynd Nota hönnun kerfis (svart) Færslur í umræðunni From dc79348118c117a718068109587a50b824a5d4ff Mon Sep 17 00:00:00 2001 From: Levi Bard Date: Tue, 29 Apr 2025 19:35:18 +0200 Subject: [PATCH 45/54] Add support for the blur filter action (#5038) Add support for the [blur](https://docs.joinmastodon.org/entities/Filter/#filter_action) filter action added in mastodon 4.4.0. Images and videos on matched posts are hidden by default, and the label reads "Filtered: ${title of applied filter}". If a matched post has a preview card, the preview image is also blurred. ~~This is draft for now until I upgrade my instance and test, I've tested so far with spoofed data.~~ Screenshot of two posts in the timeline with
blurred media. The label reads: Filtered: BLUR --- .../70.json | 1405 +++++++++++++++++ .../keylesspalace/tusky/StatusListActivity.kt | 14 +- .../tusky/adapter/StatusBaseViewHolder.java | 19 +- .../adapter/StatusDetailedViewHolder.java | 2 +- .../tusky/adapter/StatusViewHolder.java | 2 +- .../keylesspalace/tusky/appstore/Events.kt | 3 +- .../accountlist/AccountListFragment.kt | 4 +- .../conversation/ConversationViewHolder.java | 2 +- .../conversation/ConversationsFragment.kt | 4 +- .../components/filters/EditFilterActivity.kt | 34 +- .../components/filters/EditFilterViewModel.kt | 18 +- .../components/filters/FiltersAdapter.kt | 2 +- .../components/instanceinfo/InstanceInfo.kt | 1 + .../instanceinfo/InstanceInfoRepository.kt | 7 +- .../notifications/NotificationTypeMappers.kt | 5 +- .../NotificationsPagingAdapter.kt | 4 +- .../notifications/NotificationsViewModel.kt | 21 +- .../NotificationRequestDetailsFragment.kt | 4 +- ...otificationRequestDetailsRemoteMediator.kt | 6 +- .../components/report/ReportViewModel.kt | 2 +- .../components/search/SearchViewModel.kt | 6 +- .../timeline/TimelinePagingAdapter.kt | 4 +- .../timeline/TimelineTypeMappers.kt | 9 +- .../viewmodel/CachedTimelineRemoteMediator.kt | 2 +- .../viewmodel/CachedTimelineViewModel.kt | 12 +- .../NetworkTimelineRemoteMediator.kt | 6 +- .../viewmodel/NetworkTimelineViewModel.kt | 9 +- .../timeline/viewmodel/TimelineViewModel.kt | 15 +- .../viewmodel/TrendingTagsViewModel.kt | 2 +- .../components/viewthread/ThreadAdapter.kt | 2 +- .../viewthread/ViewThreadViewModel.kt | 11 +- .../keylesspalace/tusky/db/AppDatabase.java | 3 +- .../tusky/db/entity/InstanceEntity.kt | 3 + .../com/keylesspalace/tusky/entity/Filter.kt | 37 +- .../keylesspalace/tusky/entity/FilterV1.kt | 26 +- .../keylesspalace/tusky/entity/Instance.kt | 8 +- .../com/keylesspalace/tusky/entity/Status.kt | 6 + .../tusky/network/FilterModel.kt | 20 +- .../tusky/network/MastodonApi.kt | 8 +- .../keylesspalace/tusky/util/ViewDataUtils.kt | 20 +- .../tusky/viewdata/StatusViewData.kt | 2 +- .../main/res/layout/activity_edit_filter.xml | 7 + app/src/main/res/values/string-arrays.xml | 1 + app/src/main/res/values/strings.xml | 4 +- .../com/keylesspalace/tusky/FilterV1Test.kt | 33 +- .../components/compose/ComposeActivityTest.kt | 2 +- .../CachedTimelineRemoteMediatorTest.kt | 2 + .../NetworkTimelineRemoteMediatorTest.kt | 10 + 48 files changed, 1654 insertions(+), 175 deletions(-) create mode 100644 app/schemas/com.keylesspalace.tusky.db.AppDatabase/70.json diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/70.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/70.json new file mode 100644 index 000000000..30ad53a2a --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/70.json @@ -0,0 +1,1405 @@ +{ + "formatVersion": 1, + "database": { + "version": 70, + "identityHash": "f1ac7b67aa0a9a279f7f35f5817b6a17", + "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, `failedToSendNew` INTEGER NOT NULL, `scheduledAt` TEXT, `language` TEXT, `statusId` TEXT)", + "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 + }, + { + "fieldPath": "failedToSendNew", + "columnName": "failedToSendNew", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scheduledAt", + "columnName": "scheduledAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "statusId", + "columnName": "statusId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "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, `clientId` TEXT, `clientSecret` TEXT, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `profileHeaderUrl` TEXT NOT NULL DEFAULT '', `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, `notificationsUpdates` INTEGER NOT NULL, `notificationsAdmin` INTEGER NOT NULL DEFAULT true, `notificationsOther` INTEGER NOT NULL DEFAULT true, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultReplyPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `defaultPostLanguage` TEXT NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL DEFAULT 0, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `notificationMarkerId` TEXT NOT NULL DEFAULT '0', `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, `lastVisibleHomeTimelineStatusId` TEXT, `locked` INTEGER NOT NULL DEFAULT 0, `hasDirectMessageBadge` INTEGER NOT NULL DEFAULT 0, `isShowHomeBoosts` INTEGER NOT NULL, `isShowHomeReplies` INTEGER NOT NULL, `isShowHomeSelfBoosts` INTEGER 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": "clientId", + "columnName": "clientId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "clientSecret", + "columnName": "clientSecret", + "affinity": "TEXT", + "notNull": false + }, + { + "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": "profileHeaderUrl", + "columnName": "profileHeaderUrl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "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": "notificationsUpdates", + "columnName": "notificationsUpdates", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsAdmin", + "columnName": "notificationsAdmin", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "true" + }, + { + "fieldPath": "notificationsOther", + "columnName": "notificationsOther", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "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": "defaultReplyPrivacy", + "columnName": "defaultReplyPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostLanguage", + "columnName": "defaultPostLanguage", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationMarkerId", + "columnName": "notificationMarkerId", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'0'" + }, + { + "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 + }, + { + "fieldPath": "lastVisibleHomeTimelineStatusId", + "columnName": "lastVisibleHomeTimelineStatusId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "hasDirectMessageBadge", + "columnName": "hasDirectMessageBadge", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isShowHomeBoosts", + "columnName": "isShowHomeBoosts", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isShowHomeReplies", + "columnName": "isShowHomeReplies", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isShowHomeSelfBoosts", + "columnName": "isShowHomeSelfBoosts", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "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, `videoSizeLimit` INTEGER, `imageSizeLimit` INTEGER, `imageMatrixLimit` INTEGER, `maxMediaAttachments` INTEGER, `maxFields` INTEGER, `maxFieldNameLength` INTEGER, `maxFieldValueLength` INTEGER, `translationEnabled` INTEGER, `mastodonApiVersion` INTEGER, `filterV2Supported` INTEGER NOT NULL DEFAULT false, 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 + }, + { + "fieldPath": "videoSizeLimit", + "columnName": "videoSizeLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "imageSizeLimit", + "columnName": "imageSizeLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "imageMatrixLimit", + "columnName": "imageMatrixLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxMediaAttachments", + "columnName": "maxMediaAttachments", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFields", + "columnName": "maxFields", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFieldNameLength", + "columnName": "maxFieldNameLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFieldValueLength", + "columnName": "maxFieldValueLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "translationEnabled", + "columnName": "translationEnabled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mastodonApiVersion", + "columnName": "mastodonApiVersion", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "filterV2Supported", + "columnName": "filterV2Supported", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "instance" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `tuskyAccountId` INTEGER NOT NULL, `authorServerId` TEXT NOT NULL, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `editedAt` INTEGER, `emojis` TEXT NOT NULL, `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 NOT NULL, `mentions` TEXT NOT NULL, `tags` TEXT NOT NULL, `application` TEXT, `poll` TEXT, `muted` INTEGER NOT NULL, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `card` TEXT, `language` TEXT, `filtered` TEXT NOT NULL, PRIMARY KEY(`serverId`, `tuskyAccountId`), FOREIGN KEY(`authorServerId`, `tuskyAccountId`) REFERENCES `TimelineAccountEntity`(`serverId`, `tuskyAccountId`) 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": "tuskyAccountId", + "columnName": "tuskyAccountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "editedAt", + "columnName": "editedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "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": true + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": true + }, + { + "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 + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filtered", + "columnName": "filtered", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "serverId", + "tuskyAccountId" + ] + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_tuskyAccountId", + "unique": false, + "columnNames": [ + "authorServerId", + "tuskyAccountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_tuskyAccountId` ON `${TABLE_NAME}` (`authorServerId`, `tuskyAccountId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "tuskyAccountId" + ], + "referencedColumns": [ + "serverId", + "tuskyAccountId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `tuskyAccountId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `note` TEXT NOT NULL DEFAULT '', `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `tuskyAccountId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tuskyAccountId", + "columnName": "tuskyAccountId", + "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": "note", + "columnName": "note", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "serverId", + "tuskyAccountId" + ] + }, + "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_editedAt` INTEGER, `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, `s_language` 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.editedAt", + "columnName": "s_editedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "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 + }, + { + "fieldPath": "lastStatus.language", + "columnName": "s_language", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "accountId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "NotificationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tuskyAccountId` INTEGER NOT NULL, `type` TEXT, `id` TEXT NOT NULL, `accountId` TEXT, `statusId` TEXT, `reportId` TEXT, `event` TEXT, `moderationWarning` TEXT, `loading` INTEGER NOT NULL, PRIMARY KEY(`id`, `tuskyAccountId`), FOREIGN KEY(`accountId`, `tuskyAccountId`) REFERENCES `TimelineAccountEntity`(`serverId`, `tuskyAccountId`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`statusId`, `tuskyAccountId`) REFERENCES `TimelineStatusEntity`(`serverId`, `tuskyAccountId`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`reportId`, `tuskyAccountId`) REFERENCES `NotificationReportEntity`(`serverId`, `tuskyAccountId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "tuskyAccountId", + "columnName": "tuskyAccountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "statusId", + "columnName": "statusId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reportId", + "columnName": "reportId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "event", + "columnName": "event", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "moderationWarning", + "columnName": "moderationWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loading", + "columnName": "loading", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "tuskyAccountId" + ] + }, + "indices": [ + { + "name": "index_NotificationEntity_accountId_tuskyAccountId", + "unique": false, + "columnNames": [ + "accountId", + "tuskyAccountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_NotificationEntity_accountId_tuskyAccountId` ON `${TABLE_NAME}` (`accountId`, `tuskyAccountId`)" + }, + { + "name": "index_NotificationEntity_statusId_tuskyAccountId", + "unique": false, + "columnNames": [ + "statusId", + "tuskyAccountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_NotificationEntity_statusId_tuskyAccountId` ON `${TABLE_NAME}` (`statusId`, `tuskyAccountId`)" + }, + { + "name": "index_NotificationEntity_reportId_tuskyAccountId", + "unique": false, + "columnNames": [ + "reportId", + "tuskyAccountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_NotificationEntity_reportId_tuskyAccountId` ON `${TABLE_NAME}` (`reportId`, `tuskyAccountId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "accountId", + "tuskyAccountId" + ], + "referencedColumns": [ + "serverId", + "tuskyAccountId" + ] + }, + { + "table": "TimelineStatusEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "statusId", + "tuskyAccountId" + ], + "referencedColumns": [ + "serverId", + "tuskyAccountId" + ] + }, + { + "table": "NotificationReportEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "reportId", + "tuskyAccountId" + ], + "referencedColumns": [ + "serverId", + "tuskyAccountId" + ] + } + ] + }, + { + "tableName": "NotificationReportEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tuskyAccountId` INTEGER NOT NULL, `serverId` TEXT NOT NULL, `category` TEXT NOT NULL, `statusIds` TEXT, `createdAt` INTEGER NOT NULL, `targetAccountId` TEXT, PRIMARY KEY(`serverId`, `tuskyAccountId`), FOREIGN KEY(`targetAccountId`, `tuskyAccountId`) REFERENCES `TimelineAccountEntity`(`serverId`, `tuskyAccountId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "tuskyAccountId", + "columnName": "tuskyAccountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "statusIds", + "columnName": "statusIds", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "targetAccountId", + "columnName": "targetAccountId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "serverId", + "tuskyAccountId" + ] + }, + "indices": [ + { + "name": "index_NotificationReportEntity_targetAccountId_tuskyAccountId", + "unique": false, + "columnNames": [ + "targetAccountId", + "tuskyAccountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_NotificationReportEntity_targetAccountId_tuskyAccountId` ON `${TABLE_NAME}` (`targetAccountId`, `tuskyAccountId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "targetAccountId", + "tuskyAccountId" + ], + "referencedColumns": [ + "serverId", + "tuskyAccountId" + ] + } + ] + }, + { + "tableName": "HomeTimelineEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tuskyAccountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `statusId` TEXT, `reblogAccountId` TEXT, `loading` INTEGER NOT NULL, PRIMARY KEY(`id`, `tuskyAccountId`), FOREIGN KEY(`statusId`, `tuskyAccountId`) REFERENCES `TimelineStatusEntity`(`serverId`, `tuskyAccountId`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`reblogAccountId`, `tuskyAccountId`) REFERENCES `TimelineAccountEntity`(`serverId`, `tuskyAccountId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "tuskyAccountId", + "columnName": "tuskyAccountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "statusId", + "columnName": "statusId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loading", + "columnName": "loading", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "tuskyAccountId" + ] + }, + "indices": [ + { + "name": "index_HomeTimelineEntity_statusId_tuskyAccountId", + "unique": false, + "columnNames": [ + "statusId", + "tuskyAccountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_HomeTimelineEntity_statusId_tuskyAccountId` ON `${TABLE_NAME}` (`statusId`, `tuskyAccountId`)" + }, + { + "name": "index_HomeTimelineEntity_reblogAccountId_tuskyAccountId", + "unique": false, + "columnNames": [ + "reblogAccountId", + "tuskyAccountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_HomeTimelineEntity_reblogAccountId_tuskyAccountId` ON `${TABLE_NAME}` (`reblogAccountId`, `tuskyAccountId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineStatusEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "statusId", + "tuskyAccountId" + ], + "referencedColumns": [ + "serverId", + "tuskyAccountId" + ] + }, + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "reblogAccountId", + "tuskyAccountId" + ], + "referencedColumns": [ + "serverId", + "tuskyAccountId" + ] + } + ] + }, + { + "tableName": "NotificationPolicyEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tuskyAccountId` INTEGER NOT NULL, `pendingRequestsCount` INTEGER NOT NULL, `pendingNotificationsCount` INTEGER NOT NULL, PRIMARY KEY(`tuskyAccountId`))", + "fields": [ + { + "fieldPath": "tuskyAccountId", + "columnName": "tuskyAccountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pendingRequestsCount", + "columnName": "pendingRequestsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pendingNotificationsCount", + "columnName": "pendingNotificationsCount", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "tuskyAccountId" + ] + }, + "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, 'f1ac7b67aa0a9a279f7f35f5817b6a17')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt b/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt index d0cbc949d..b6dfeaff3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt @@ -204,7 +204,7 @@ class StatusListActivity : BottomSheetActivity() { { filters -> mutedFilter = filters.firstOrNull { filter -> // TODO shouldn't this be an exact match (only one keyword; exactly the hashtag)? - filter.context.contains(Filter.Kind.HOME.kind) && filter.title == hashedTag + filter.context.contains(Filter.Kind.HOME) && filter.title == hashedTag } updateTagMuteState(mutedFilter != null) }, @@ -250,8 +250,8 @@ class StatusListActivity : BottomSheetActivity() { mastodonApi.createFilter( title = "#$tag", - context = listOf(Filter.Kind.HOME.kind), - filterAction = Filter.Action.WARN.action, + context = listOf(Filter.Kind.HOME), + filterAction = Filter.Action.WARN, expiresIn = FilterExpiration.never ).fold( { filter -> @@ -286,7 +286,7 @@ class StatusListActivity : BottomSheetActivity() { ).fold( { filter -> mutedFilterV1 = filter - eventHub.dispatch(FilterUpdatedEvent(filter.context)) + eventHub.dispatch(FilterUpdatedEvent(filter.context.map { Filter.Kind.valueOf(it) })) filterCreateSuccess = true }, { throwable2 -> @@ -344,7 +344,7 @@ class StatusListActivity : BottomSheetActivity() { // This filter exists in multiple contexts, just remove the home context mastodonApi.updateFilter( id = filter.id, - context = filter.context.filter { it != Filter.Kind.HOME.kind } + context = filter.context.filterNot { it == Filter.Kind.HOME } ) } else { mastodonApi.deleteFilter(filter.id) @@ -356,7 +356,7 @@ class StatusListActivity : BottomSheetActivity() { mastodonApi.updateFilterV1( id = filter.id, phrase = filter.phrase, - context = filter.context.filter { it != Filter.Kind.HOME.kind }, + context = filter.context.filterNot { it == Filter.Kind.HOME.kind }, irreversible = null, wholeWord = null, expiresIn = FilterExpiration.never @@ -372,7 +372,7 @@ class StatusListActivity : BottomSheetActivity() { result?.fold( { updateTagMuteState(false) - eventHub.dispatch(FilterUpdatedEvent(listOf(Filter.Kind.HOME.kind))) + eventHub.dispatch(FilterUpdatedEvent(listOf(Filter.Kind.HOME))) mutedFilterV1 = null mutedFilter = null diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java index e9eece7bc..b28bcc702 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java @@ -45,6 +45,7 @@ import com.keylesspalace.tusky.ViewMediaActivity; import com.keylesspalace.tusky.entity.Attachment; import com.keylesspalace.tusky.entity.Attachment.Focus; import com.keylesspalace.tusky.entity.Attachment.MetaData; +import com.keylesspalace.tusky.entity.Filter; import com.keylesspalace.tusky.entity.PreviewCard; import com.keylesspalace.tusky.entity.Emoji; import com.keylesspalace.tusky.entity.HashTag; @@ -277,7 +278,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { this.setTextVisible(sensitive, expanded, status, statusDisplayOptions, listener); - setupCard(status, expanded, statusDisplayOptions.cardViewMode(), statusDisplayOptions, listener); + setupCard(status, expanded, !status.isShowingContent(), statusDisplayOptions.cardViewMode(), statusDisplayOptions, listener); } private void setTextVisible(boolean sensitive, @@ -523,7 +524,8 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { boolean sensitive, final @NonNull StatusActionListener listener, boolean showingContent, - boolean useBlurhash + boolean useBlurhash, + Filter filter ) { mediaPreview.setVisibility(View.VISIBLE); @@ -559,7 +561,9 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { final CharSequence formattedDescription = AttachmentHelper.getFormattedDescription(attachment, imageView.getContext()); setAttachmentClickListener(imageView, listener, i, formattedDescription, true); - if (sensitive) { + if (filter != null) { + sensitiveMediaWarning.setText(sensitiveMediaWarning.getContext().getString(R.string.status_filter_placeholder_label_format, filter.getTitle())); + } else if (sensitive) { sensitiveMediaWarning.setText(R.string.post_sensitive_media_title); } else { sensitiveMediaWarning.setText(R.string.post_media_hidden_title); @@ -812,7 +816,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { } else if (statusDisplayOptions.mediaPreviewEnabled() && hasPreviewableAttachment(attachments)) { mediaContainer.setVisibility(View.VISIBLE); - setMediaPreviews(attachments, sensitive, listener, status.isShowingContent(), statusDisplayOptions.useBlurhash()); + setMediaPreviews(attachments, sensitive, listener, status.isShowingContent(), statusDisplayOptions.useBlurhash(), status.getFilter()); if (attachments.isEmpty()) { hideSensitiveMediaWarning(); @@ -830,7 +834,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { hideSensitiveMediaWarning(); } - setupCard(status, status.isExpanded(), statusDisplayOptions.cardViewMode(), statusDisplayOptions, listener); + setupCard(status, status.isExpanded(), !status.isShowingContent(), statusDisplayOptions.cardViewMode(), statusDisplayOptions, listener); setupButtons(listener, actionable.getAccount().getId(), status.getContent().toString(), statusDisplayOptions); @@ -855,7 +859,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { setMetaData(status, statusDisplayOptions, listener); if (status.getStatus().getCard() != null && status.getStatus().getCard().getPublishedAt() != null) { // there is a preview card showing the published time, we need to refresh it as well - setupCard(status, status.isExpanded(), statusDisplayOptions.cardViewMode(), statusDisplayOptions, listener); + setupCard(status, status.isExpanded(), !status.isShowingContent(), statusDisplayOptions.cardViewMode(), statusDisplayOptions, listener); } break; } @@ -1157,6 +1161,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { protected void setupCard( final @NonNull StatusViewData.Concrete status, boolean expanded, + boolean blurMedia, final @NonNull CardViewMode cardViewMode, final @NonNull StatusDisplayOptions statusDisplayOptions, final @NonNull StatusActionListener listener @@ -1236,7 +1241,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { // Statuses from other activitypub sources can be marked sensitive even if there's no media, // so let's blur the preview in that case // If media previews are disabled, show placeholder for cards as well - if (statusDisplayOptions.mediaPreviewEnabled() && !actionable.getSensitive() && !TextUtils.isEmpty(card.getImage())) { + if (statusDisplayOptions.mediaPreviewEnabled() && !blurMedia && !actionable.getSensitive() && !TextUtils.isEmpty(card.getImage())) { int radius = context.getResources().getDimensionPixelSize(R.dimen.inner_card_radius); ShapeAppearanceModel.Builder cardImageShape = ShapeAppearanceModel.builder(); diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java index 1a0c42797..eeba661fd 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java @@ -149,7 +149,7 @@ public class StatusDetailedViewHolder extends StatusBaseViewHolder { status; super.setupWithStatus(uncollapsedStatus, listener, statusDisplayOptions, payloads, showStatusInfo); - setupCard(uncollapsedStatus, status.isExpanded(), CardViewMode.FULL_WIDTH, statusDisplayOptions, listener); // Always show card for detailed status + setupCard(uncollapsedStatus, status.isExpanded(), !status.isShowingContent(), CardViewMode.FULL_WIDTH, statusDisplayOptions, listener); // Always show card for detailed status if (payloads.isEmpty()) { Status actionable = uncollapsedStatus.getActionable(); diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java index 2a63531ef..3d0235e25 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java @@ -69,7 +69,7 @@ public class StatusViewHolder extends StatusBaseViewHolder { setupCollapsedState(sensitive, expanded, status, listener); - if (!showStatusInfo || status.getFilterAction() == Filter.Action.WARN) { + if (!showStatusInfo || (status.getFilter() != null && status.getFilter().getAction() == Filter.Action.WARN)) { hideStatusInfo(); } else { Status rebloggingStatus = status.getRebloggingStatus(); diff --git a/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt b/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt index 1b4070dd2..e7363fa0f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt +++ b/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt @@ -1,6 +1,7 @@ package com.keylesspalace.tusky.appstore import com.keylesspalace.tusky.entity.Account +import com.keylesspalace.tusky.entity.Filter import com.keylesspalace.tusky.entity.Notification import com.keylesspalace.tusky.entity.Poll import com.keylesspalace.tusky.entity.Status @@ -18,7 +19,7 @@ data class PollVoteEvent(val statusId: String, val poll: Poll) : Event data class PollShowResultsEvent(val statusId: String) : Event data class DomainMuteEvent(val instance: String) : Event data class AnnouncementReadEvent(val announcementId: String) : Event -data class FilterUpdatedEvent(val filterContext: List) : Event +data class FilterUpdatedEvent(val filterContext: List) : Event data class NewNotificationsEvent( val accountId: String, val notifications: List diff --git a/app/src/main/java/com/keylesspalace/tusky/components/accountlist/AccountListFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/accountlist/AccountListFragment.kt index ffc7ab4d6..1bf2c0557 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/accountlist/AccountListFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/accountlist/AccountListFragment.kt @@ -205,8 +205,8 @@ class AccountListFragment : viewModel.unblock(id) } - override fun onRespondToFollowRequest(accept: Boolean, id: String, position: Int) { - viewModel.respondToFollowRequest(accept, id) + override fun onRespondToFollowRequest(accept: Boolean, accountIdRequestingFollow: String, position: Int) { + viewModel.respondToFollowRequest(accept, accountIdRequestingFollow) } companion object { 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 d94f60113..e394ff263 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 @@ -96,7 +96,7 @@ public class ConversationViewHolder extends StatusBaseViewHolder { mediaContainer.setVisibility(View.VISIBLE); setMediaPreviews(attachments, sensitive, listener, statusViewData.isShowingContent(), - statusDisplayOptions.useBlurhash()); + statusDisplayOptions.useBlurhash(), statusViewData.getFilter()); if (attachments.isEmpty()) { hideSensitiveMediaWarning(); 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 60eb749bb..ec56ebff2 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 @@ -242,9 +242,9 @@ class ConversationsFragment : } } - override fun onBookmark(favourite: Boolean, position: Int) { + override fun onBookmark(bookmark: Boolean, position: Int) { adapter?.peek(position)?.let { conversation -> - viewModel.bookmark(favourite, conversation) + viewModel.bookmark(bookmark, conversation) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/filters/EditFilterActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/filters/EditFilterActivity.kt index 414fb1b88..5770cd2c5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/filters/EditFilterActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/filters/EditFilterActivity.kt @@ -35,6 +35,7 @@ import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.R import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.FilterUpdatedEvent +import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository import com.keylesspalace.tusky.databinding.ActivityEditFilterBinding import com.keylesspalace.tusky.databinding.DialogFilterBinding import com.keylesspalace.tusky.entity.Filter @@ -56,6 +57,9 @@ class EditFilterActivity : BaseActivity() { @Inject lateinit var eventHub: EventHub + @Inject + lateinit var instanceInfoRepository: InstanceInfoRepository + private val binding by viewBinding(ActivityEditFilterBinding::inflate) private val viewModel: EditFilterViewModel by viewModels() @@ -67,7 +71,7 @@ class EditFilterActivity : BaseActivity() { super.onCreate(savedInstanceState) originalFilter = intent.getParcelableExtraCompat(FILTER_TO_EDIT) - filter = originalFilter ?: Filter("", "", listOf(), null, Filter.Action.WARN.action, listOf()) + filter = originalFilter ?: Filter(context = emptyList(), action = Filter.Action.WARN) binding.apply { contextSwitches = mapOf( filterContextHome to Filter.Kind.HOME, @@ -124,14 +128,17 @@ class EditFilterActivity : BaseActivity() { viewModel.setTitle(editable.toString()) validateSaveButton() } - binding.filterActionWarn.setOnCheckedChangeListener { _, checked -> - viewModel.setAction( - if (checked) { - Filter.Action.WARN - } else { - Filter.Action.HIDE - } - ) + + // blur filter is supported in mastodon api version 5+ + val blurFilterSupported = instanceInfoRepository.cachedInstanceInfoOrFallback.mastodonApiVersion?.let { it >= 5 } == true + binding.filterActionBlur.visible(blurFilterSupported) + binding.filterActionGroup.setOnCheckedChangeListener { _, checkedId -> + val action = when (checkedId) { + R.id.filter_action_blur -> Filter.Action.BLUR + R.id.filter_action_hide -> Filter.Action.HIDE + else -> Filter.Action.WARN + } + viewModel.setAction(action) } binding.filterDurationDropDown.onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ -> viewModel.setDuration( @@ -178,6 +185,7 @@ class EditFilterActivity : BaseActivity() { lifecycleScope.launch { viewModel.action.collect { action -> when (action) { + Filter.Action.BLUR -> binding.filterActionBlur.isChecked = true Filter.Action.HIDE -> binding.filterActionHide.isChecked = true else -> binding.filterActionWarn.isChecked = true } @@ -299,14 +307,14 @@ class EditFilterActivity : BaseActivity() { if (viewModel.saveChanges(this@EditFilterActivity)) { finish() // Possibly affected contexts: any context affected by the original filter OR any context affected by the updated filter - val affectedContexts = viewModel.contexts.value.map { - it.kind - }.union(originalFilter?.context ?: listOf()).distinct() + val affectedContexts = viewModel.contexts.value + .union(originalFilter?.context.orEmpty()) + .distinct() eventHub.dispatch(FilterUpdatedEvent(affectedContexts)) } else { Snackbar.make( binding.root, - getString(R.string.error_deleting_filter, viewModel.title.value), + getString(R.string.error_saving_filter, viewModel.title.value), Snackbar.LENGTH_SHORT ).show() } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/filters/EditFilterViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/filters/EditFilterViewModel.kt index 881fd70e9..c34306d5d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/filters/EditFilterViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/filters/EditFilterViewModel.kt @@ -60,7 +60,7 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi) : ViewModel( } else { -1 } - _contexts.value = filter.kinds + _contexts.value = filter.context } fun addKeyword(keyword: FilterKeyword) { @@ -109,10 +109,10 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi) : ViewModel( } suspend fun saveChanges(context: Context): Boolean { - val contexts = _contexts.value.map { it.kind } + val contexts = _contexts.value val title = _title.value val durationIndex = _duration.value - val action = _action.value.action + val action = _action.value return withContext(viewModelScope.coroutineContext) { originalFilter?.let { filter -> @@ -123,8 +123,8 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi) : ViewModel( private suspend fun createFilter( title: String, - contexts: List, - action: String, + contexts: List, + action: Filter.Action, durationIndex: Int, context: Context ): Boolean { @@ -149,7 +149,7 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi) : ViewModel( return ( throwable.isHttpNotFound() && // Endpoint not found, fall back to v1 api - createFilterV1(contexts, expiration) + createFilterV1(contexts.map(Filter.Kind::kind), expiration) ) } ) @@ -158,8 +158,8 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi) : ViewModel( private suspend fun updateFilter( originalFilter: Filter, title: String, - contexts: List, - action: String, + contexts: List, + action: Filter.Action, durationIndex: Int, context: Context ): Boolean { @@ -189,7 +189,7 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi) : ViewModel( { throwable -> if (throwable.isHttpNotFound()) { // Endpoint not found, fall back to v1 api - if (updateFilterV1(contexts, expiration)) { + if (updateFilterV1(contexts.map(Filter.Kind::kind), expiration)) { return true } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/filters/FiltersAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/filters/FiltersAdapter.kt index 96d51fc6e..72c294c19 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/filters/FiltersAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/filters/FiltersAdapter.kt @@ -43,7 +43,7 @@ class FiltersAdapter(val listener: FiltersListener, val filters: List) : binding.textSecondary.text = context.getString( R.string.filter_description_format, actions.getOrNull(filter.action.ordinal - 1), - filter.context.map { contexts.getOrNull(Filter.Kind.from(it).ordinal) }.joinToString("/") + filter.context.map { contexts.getOrNull(it.ordinal) }.joinToString("/") ) binding.delete.setOnClickListener { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfo.kt b/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfo.kt index 33a1122f3..0b3fdaeb7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfo.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfo.kt @@ -31,4 +31,5 @@ data class InstanceInfo( val maxFieldValueLength: Int?, val version: String?, val translationEnabled: Boolean?, + val mastodonApiVersion: Int?, ) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfoRepository.kt b/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfoRepository.kt index 171a0f31b..d857bfb02 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfoRepository.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfoRepository.kt @@ -145,7 +145,8 @@ class InstanceInfoRepository @Inject constructor( maxFieldNameLength = this?.maxFieldNameLength, maxFieldValueLength = this?.maxFieldValueLength, version = this?.version, - translationEnabled = this?.translationEnabled + translationEnabled = this?.translationEnabled, + mastodonApiVersion = this?.mastodonApiVersion, ) private fun Instance.toEntity() = InstanceInfoEntity( @@ -173,7 +174,8 @@ class InstanceInfoRepository @Inject constructor( maxFields = this.configuration?.accounts?.maxProfileFields ?: this.pleroma?.metadata?.fieldLimits?.maxFields, maxFieldNameLength = this.pleroma?.metadata?.fieldLimits?.nameLength, maxFieldValueLength = this.pleroma?.metadata?.fieldLimits?.valueLength, - translationEnabled = this.configuration?.translation?.enabled + translationEnabled = this.configuration?.translation?.enabled, + mastodonApiVersion = this.apiVersions?.mastodon, ) private fun InstanceV1.toEntity(instanceName: String) = @@ -202,6 +204,7 @@ class InstanceInfoRepository @Inject constructor( maxFieldNameLength = this.pleroma?.metadata?.fieldLimits?.nameLength, maxFieldValueLength = this.pleroma?.metadata?.fieldLimits?.valueLength, translationEnabled = null, + mastodonApiVersion = null, ) companion object { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationTypeMappers.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationTypeMappers.kt index 851f815cd..f455a0265 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationTypeMappers.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationTypeMappers.kt @@ -22,6 +22,7 @@ import com.keylesspalace.tusky.db.entity.NotificationDataEntity import com.keylesspalace.tusky.db.entity.NotificationEntity import com.keylesspalace.tusky.db.entity.NotificationReportEntity import com.keylesspalace.tusky.db.entity.TimelineAccountEntity +import com.keylesspalace.tusky.entity.Filter import com.keylesspalace.tusky.entity.Notification import com.keylesspalace.tusky.entity.Report import com.keylesspalace.tusky.util.toViewData @@ -61,6 +62,7 @@ fun Notification.toViewData( isShowingContent: Boolean, isExpanded: Boolean, isCollapsed: Boolean, + filter: Filter?, ): NotificationViewData.Concrete = NotificationViewData.Concrete( id = id, type = type, @@ -68,7 +70,8 @@ fun Notification.toViewData( statusViewData = status?.toViewData( isShowingContent = isShowingContent, isExpanded = isExpanded, - isCollapsed = isCollapsed + isCollapsed = isCollapsed, + filter = filter, ), report = report, moderationWarning = moderationWarning, diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsPagingAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsPagingAdapter.kt index 394791b04..a3cdce87c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsPagingAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsPagingAdapter.kt @@ -83,13 +83,13 @@ class NotificationsPagingAdapter( is NotificationViewData.Concrete -> { when (notification.type) { Notification.Type.Mention, - Notification.Type.Poll -> if (notification.statusViewData?.filterAction == Filter.Action.WARN) { + Notification.Type.Poll -> if (notification.statusViewData?.filter?.action == Filter.Action.WARN) { VIEW_TYPE_STATUS_FILTERED } else { VIEW_TYPE_STATUS } Notification.Type.Status, - Notification.Type.Update -> if (notification.statusViewData?.filterAction == Filter.Action.WARN) { + Notification.Type.Update -> if (notification.statusViewData?.filter?.action == Filter.Action.WARN) { VIEW_TYPE_STATUS_FILTERED } else { VIEW_TYPE_STATUS_NOTIFICATION diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsViewModel.kt index eacc28225..a557e9324 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsViewModel.kt @@ -117,7 +117,7 @@ class NotificationsViewModel @Inject constructor( val translation = translations[notification.status?.serverId] notification.toViewData(translation = translation) }.filter { notificationViewData -> - shouldFilterStatus(notificationViewData) != Filter.Action.HIDE + shouldFilterStatus(notificationViewData)?.action != Filter.Action.HIDE } } } @@ -131,7 +131,7 @@ class NotificationsViewModel @Inject constructor( if (event is PreferenceChangedEvent) { onPreferenceChanged(event.preferenceKey) } - if (event is FilterUpdatedEvent && event.filterContext.contains(Filter.Kind.NOTIFICATIONS.kind)) { + if (event is FilterUpdatedEvent && event.filterContext.contains(Filter.Kind.NOTIFICATIONS)) { filterModel.init(Filter.Kind.NOTIFICATIONS) refreshTrigger.value += 1 } @@ -165,21 +165,21 @@ class NotificationsViewModel @Inject constructor( } } - private fun shouldFilterStatus(notificationViewData: NotificationViewData): Filter.Action { + private fun shouldFilterStatus(notificationViewData: NotificationViewData): Filter? { return when ((notificationViewData as? NotificationViewData.Concrete)?.type) { Notification.Type.Mention, Notification.Type.Poll, Notification.Type.Status, Notification.Type.Update -> { val account = activeAccountFlow.value notificationViewData.statusViewData?.let { statusViewData -> if (statusViewData.status.account.id == account?.accountId) { - return Filter.Action.NONE + return null } - statusViewData.filterAction = filterModel.shouldFilterStatus(statusViewData.actionable) - return statusViewData.filterAction + statusViewData.filter = filterModel.shouldFilterStatus(statusViewData.actionable) + return statusViewData.filter } - Filter.Action.NONE + null } - else -> Filter.Action.NONE + else -> null } } @@ -346,10 +346,7 @@ class NotificationsViewModel @Inject constructor( return@launch } - val account = activeAccountFlow.value - if (account == null) { - return@launch - } + val account = activeAccountFlow.value ?: return@launch val statusDao = db.timelineStatusDao() val accountDao = db.timelineAccountDao() diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/requests/details/NotificationRequestDetailsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/requests/details/NotificationRequestDetailsFragment.kt index 3b6514a97..3d57125c6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/notifications/requests/details/NotificationRequestDetailsFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/requests/details/NotificationRequestDetailsFragment.kt @@ -285,9 +285,9 @@ class NotificationRequestDetailsFragment : SFragment(R.layout.fragment_notificat // not needed, blocking via the more menu on statuses is handled in SFragment } - override fun onRespondToFollowRequest(accept: Boolean, id: String, position: Int) { + override fun onRespondToFollowRequest(accept: Boolean, accountIdRequestingFollow: String, position: Int) { val notification = adapter?.peek(position) ?: return - viewModel.respondToFollowRequest(accept, accountId = id, notification = notification) + viewModel.respondToFollowRequest(accept, accountId = accountIdRequestingFollow, notification = notification) } override fun onDestroyView() { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/requests/details/NotificationRequestDetailsRemoteMediator.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/requests/details/NotificationRequestDetailsRemoteMediator.kt index c1f8ca984..7ecee658a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/notifications/requests/details/NotificationRequestDetailsRemoteMediator.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/requests/details/NotificationRequestDetailsRemoteMediator.kt @@ -20,6 +20,7 @@ import androidx.paging.LoadType import androidx.paging.PagingState import androidx.paging.RemoteMediator import com.keylesspalace.tusky.components.notifications.toViewData +import com.keylesspalace.tusky.entity.Filter import com.keylesspalace.tusky.entity.Notification import com.keylesspalace.tusky.util.HttpHeaderLink import com.keylesspalace.tusky.viewdata.NotificationViewData @@ -70,9 +71,10 @@ class NotificationRequestDetailsRemoteMediator( val alwaysOpenSpoiler = viewModel.accountManager.activeAccount?.alwaysOpenSpoiler == false val notificationData = notifications.map { notification -> notification.toViewData( - isShowingContent = alwaysShowSensitiveMedia, + isShowingContent = notification.status?.shouldShowContent(alwaysShowSensitiveMedia, Filter.Kind.NOTIFICATIONS) ?: true, isExpanded = alwaysOpenSpoiler, - true + isCollapsed = true, + filter = notification.status?.getApplicableFilter(Filter.Kind.NOTIFICATIONS), ) } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt index 644ed122c..5b4b0b96a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt @@ -87,7 +87,7 @@ class ReportViewModel @Inject constructor( .map { pagingData -> /* TODO: refactor reports to use the isShowingContent / isExpanded / isCollapsed attributes from StatusViewData.Concrete instead of StatusViewState */ - pagingData.map { status -> status.toViewData(false, false, false) } + pagingData.map { status -> status.toViewData(false, false, false, filter = null) } } .cachedIn(viewModelScope) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt index ebdd38e69..ab5c76a94 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt @@ -30,6 +30,7 @@ import com.keylesspalace.tusky.components.search.adapter.SearchPagingSourceFacto import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.entity.AccountEntity import com.keylesspalace.tusky.entity.DeletedStatus +import com.keylesspalace.tusky.entity.Filter import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.usecase.TimelineCases @@ -70,9 +71,10 @@ class SearchViewModel @Inject constructor( SearchPagingSourceFactory(mastodonApi, SearchType.Status, loadedStatuses) { it.statuses.map { status -> status.toViewData( - isShowingContent = alwaysShowSensitiveMedia || !status.actionableStatus.sensitive, + isShowingContent = status.shouldShowContent(alwaysShowSensitiveMedia, Filter.Kind.PUBLIC), isExpanded = alwaysOpenSpoiler, - isCollapsed = true + isCollapsed = true, + filter = status.getApplicableFilter(Filter.Kind.PUBLIC), ) }.apply { loadedStatuses.addAll(this) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelinePagingAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelinePagingAdapter.kt index e6783a624..0c401ba5a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelinePagingAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelinePagingAdapter.kt @@ -84,7 +84,7 @@ class TimelinePagingAdapter( val holder = viewHolder as PlaceholderViewHolder holder.setup(viewData.isLoading) } else if (viewData is StatusViewData.Concrete) { - if (viewData.filterAction == Filter.Action.WARN) { + if (viewData.filter?.action == Filter.Action.WARN) { val holder = viewHolder as FilteredStatusViewHolder holder.bind(viewData) } else { @@ -104,7 +104,7 @@ class TimelinePagingAdapter( val viewData = getItem(position) return if (viewData is StatusViewData.Placeholder) { VIEW_TYPE_PLACEHOLDER - } else if (viewData?.filterAction == Filter.Action.WARN) { + } else if (viewData?.filter?.action == Filter.Action.WARN) { VIEW_TYPE_STATUS_FILTERED } else { VIEW_TYPE_STATUS diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineTypeMappers.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineTypeMappers.kt index b342dc2f6..b7dcf6e46 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineTypeMappers.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineTypeMappers.kt @@ -19,6 +19,7 @@ import com.keylesspalace.tusky.db.entity.HomeTimelineData import com.keylesspalace.tusky.db.entity.HomeTimelineEntity import com.keylesspalace.tusky.db.entity.TimelineAccountEntity import com.keylesspalace.tusky.db.entity.TimelineStatusEntity +import com.keylesspalace.tusky.entity.Filter import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.entity.TimelineAccount import com.keylesspalace.tusky.viewdata.StatusViewData @@ -143,7 +144,11 @@ fun TimelineStatusEntity.toStatus( filtered = filtered, ) -fun HomeTimelineData.toViewData(isDetailed: Boolean = false, translation: TranslationViewData? = null): StatusViewData { +fun HomeTimelineData.toViewData( + isDetailed: Boolean = false, + translation: TranslationViewData? = null, + filter: Filter? = null, +): StatusViewData { if (this.account == null || this.status == null) { return StatusViewData.Placeholder(this.id, loading) } @@ -195,5 +200,5 @@ fun HomeTimelineData.toViewData(isDetailed: Boolean = false, translation: Transl isDetailed = isDetailed, repliedToAccount = repliedToAccount?.toAccount(), translation = translation, - ) + ).apply { this.filter = filter } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineRemoteMediator.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineRemoteMediator.kt index 13a47848f..3a099bd2c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineRemoteMediator.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineRemoteMediator.kt @@ -161,7 +161,7 @@ class CachedTimelineRemoteMediator( } val expanded = oldStatus?.expanded ?: activeAccount.alwaysOpenSpoiler - val contentShowing = oldStatus?.contentShowing ?: (activeAccount.alwaysShowSensitiveMedia || !status.actionableStatus.sensitive) + val contentShowing = oldStatus?.contentShowing ?: status.shouldShowContent(activeAccount.alwaysShowSensitiveMedia, viewModel.kind.toFilterKind()) val contentCollapsed = oldStatus?.contentCollapsed != false statusDao.insert( diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineViewModel.kt index 12ffafa7e..d2df03cea 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineViewModel.kt @@ -34,6 +34,7 @@ import com.keylesspalace.tusky.components.preference.PreferencesFragment.Reading import com.keylesspalace.tusky.components.preference.PreferencesFragment.ReadingOrder.OLDEST_FIRST import com.keylesspalace.tusky.components.timeline.Placeholder import com.keylesspalace.tusky.components.timeline.toEntity +import com.keylesspalace.tusky.components.timeline.toStatus import com.keylesspalace.tusky.components.timeline.toViewData import com.keylesspalace.tusky.components.timeline.util.ifExpected import com.keylesspalace.tusky.db.AccountManager @@ -100,12 +101,15 @@ class CachedTimelineViewModel @Inject constructor( .combine(translations) { pagingData, translations -> pagingData.map { timelineData -> val translation = translations[timelineData.status?.serverId] + val status = timelineData.account?.let { timelineData.status?.toStatus(it) } + val filter = status?.let { shouldFilterStatus(it) } timelineData.toViewData( isDetailed = false, - translation = translation + translation = translation, + filter = filter, ) }.filter { statusViewData -> - shouldFilterStatus(statusViewData) != Filter.Action.HIDE + statusViewData.filter?.action != Filter.Action.HIDE } } .flowOn(Dispatchers.Default) @@ -207,8 +211,8 @@ class CachedTimelineViewModel @Inject constructor( status.actionableStatus.toEntity( tuskyAccountId = accountId, expanded = account.alwaysOpenSpoiler, - contentShowing = account.alwaysShowSensitiveMedia || !status.actionableStatus.sensitive, - contentCollapsed = true + contentShowing = status.shouldShowContent(account.alwaysShowSensitiveMedia, kind.toFilterKind()), + contentCollapsed = true, ) ) timelineDao.insertHomeTimelineItem( diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineRemoteMediator.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineRemoteMediator.kt index 566274afb..4207ac85e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineRemoteMediator.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineRemoteMediator.kt @@ -74,14 +74,16 @@ class NetworkTimelineRemoteMediator( s.asStatusOrNull()?.id == status.id }?.asStatusOrNull() - val contentShowing = oldStatus?.isShowingContent ?: (activeAccount.alwaysShowSensitiveMedia || !status.actionableStatus.sensitive) + val filter = oldStatus?.filter ?: status.getApplicableFilter(viewModel.kind.toFilterKind()) + val contentShowing = oldStatus?.isShowingContent ?: status.shouldShowContent(activeAccount.alwaysShowSensitiveMedia, viewModel.kind.toFilterKind()) val expanded = oldStatus?.isExpanded ?: activeAccount.alwaysOpenSpoiler val contentCollapsed = oldStatus?.isCollapsed != false status.toViewData( isShowingContent = contentShowing, isExpanded = expanded, - isCollapsed = contentCollapsed + isCollapsed = contentCollapsed, + filter = filter, ) } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineViewModel.kt index fd1d40bce..647c762ea 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineViewModel.kt @@ -102,7 +102,9 @@ class NetworkTimelineViewModel @Inject constructor( ).flow .map { pagingData -> pagingData.filter(Dispatchers.Default.asExecutor()) { statusViewData -> - shouldFilterStatus(statusViewData) != Filter.Action.HIDE + statusViewData.asStatusOrNull()?.actionable?.let { + shouldFilterStatus(it)?.action != Filter.Action.HIDE + } ?: true } } .flowOn(Dispatchers.Default) @@ -221,9 +223,10 @@ class NetworkTimelineViewModel @Inject constructor( val activeAccount = accountManager.activeAccount!! val data: MutableList = statuses.map { status -> status.toViewData( - isShowingContent = activeAccount.alwaysShowSensitiveMedia || !status.actionableStatus.sensitive, + isShowingContent = status.shouldShowContent(activeAccount.alwaysShowSensitiveMedia, kind.toFilterKind()), isExpanded = activeAccount.alwaysOpenSpoiler, - isCollapsed = true + isCollapsed = true, + filter = status.getApplicableFilter(kind.toFilterKind()), ) }.toMutableList() diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/TimelineViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/TimelineViewModel.kt index f8546a1b0..d8ae99499 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/TimelineViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/TimelineViewModel.kt @@ -178,20 +178,18 @@ abstract class TimelineViewModel( /** Triggered when currently displayed data must be reloaded. */ protected abstract suspend fun invalidate() - protected fun shouldFilterStatus(statusViewData: StatusViewData): Filter.Action { - val status = statusViewData.asStatusOrNull()?.status ?: return Filter.Action.NONE + protected fun shouldFilterStatus(status: Status): Filter? { return if ( (status.isReply && filterRemoveReplies) || (status.reblog != null && filterRemoveReblogs) || (status.account.id == status.reblog?.account?.id && filterRemoveSelfReblogs) ) { - Filter.Action.HIDE + Filter(context = listOf(kind.toFilterKind()), action = Filter.Action.HIDE) } else if (status.actionableStatus.account.id == activeAccountFlow.value?.accountId) { // Mastodon filters don't apply for own posts - Filter.Action.NONE + null } else { - statusViewData.filterAction = filterModel.shouldFilterStatus(status.actionableStatus) - statusViewData.filterAction + filterModel.shouldFilterStatus(status.actionableStatus) } } @@ -244,9 +242,8 @@ abstract class TimelineViewModel( private const val TAG = "TimelineVM" internal const val LOAD_AT_ONCE = 30 - fun filterContextMatchesKind(kind: Kind, filterContext: List): Boolean { - return filterContext.contains(kind.toFilterKind().kind) - } + fun filterContextMatchesKind(kind: Kind, filterContext: List): Boolean = + filterContext.contains(kind.toFilterKind()) } enum class Kind { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/trending/viewmodel/TrendingTagsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/trending/viewmodel/TrendingTagsViewModel.kt index ea8b4b092..7a8aba5eb 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/trending/viewmodel/TrendingTagsViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/trending/viewmodel/TrendingTagsViewModel.kt @@ -95,7 +95,7 @@ class TrendingTagsViewModel @Inject constructor( TrendingTagsUiState(emptyList(), LoadingState.LOADED) } else { val homeFilters = deferredFilters.await().getOrNull()?.filter { filter -> - filter.context.contains(Filter.Kind.HOME.kind) + filter.context.contains(Filter.Kind.HOME) } val tags = tagResponse .filter { tag -> diff --git a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ThreadAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ThreadAdapter.kt index 0b5f7d458..0ccc44fb3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ThreadAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ThreadAdapter.kt @@ -71,7 +71,7 @@ class ThreadAdapter( val viewData = getItem(position) return if (viewData.isDetailed) { VIEW_TYPE_STATUS_DETAILED - } else if (viewData.filterAction == Filter.Action.WARN) { + } else if (viewData.filter?.action == Filter.Action.WARN) { VIEW_TYPE_STATUS_FILTERED } else { VIEW_TYPE_STATUS diff --git a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadViewModel.kt index 717a0fc26..b17dfe03b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadViewModel.kt @@ -312,6 +312,7 @@ class ViewThreadViewModel @Inject constructor( isCollapsed = viewData.isCollapsed, isDetailed = viewData.isDetailed, translation = viewData.translation, + filter = viewData.filter, ) } } @@ -421,8 +422,8 @@ class ViewThreadViewModel @Inject constructor( if (status.isDetailed || status.status.account.id == activeAccount.accountId) { true } else { - status.filterAction = filterModel.shouldFilterStatus(status.status) - status.filterAction != Filter.Action.HIDE + status.filter = filterModel.shouldFilterStatus(status.status) + status.filter?.action != Filter.Action.HIDE } } } @@ -432,11 +433,11 @@ class ViewThreadViewModel @Inject constructor( it.id == this.id } return toViewData( - isShowingContent = oldStatus?.isShowingContent - ?: (alwaysShowSensitiveMedia || !actionableStatus.sensitive), + isShowingContent = oldStatus?.isShowingContent ?: actionableStatus.shouldShowContent(alwaysShowSensitiveMedia, Filter.Kind.THREAD), isExpanded = oldStatus?.isExpanded ?: alwaysOpenSpoiler, isCollapsed = oldStatus?.isCollapsed ?: !isDetailed, - isDetailed = oldStatus?.isDetailed ?: isDetailed + isDetailed = oldStatus?.isDetailed ?: isDetailed, + filter = oldStatus?.filter ?: actionableStatus.getApplicableFilter(Filter.Kind.THREAD), ) } 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 776446025..660e90743 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java +++ b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java @@ -65,7 +65,7 @@ import java.io.File; }, // Note: Starting with version 54, database versions in Tusky are always even. // This is to reserve odd version numbers for use by forks. - version = 68, + version = 70, autoMigrations = { @AutoMigration(from = 48, to = 49), @AutoMigration(from = 49, to = 50, spec = AppDatabase.MIGRATION_49_50.class), @@ -76,6 +76,7 @@ import java.io.File; @AutoMigration(from = 62, to = 64), // filterV2Available in InstanceEntity @AutoMigration(from = 64, to = 66), // added profileHeaderUrl to AccountEntity @AutoMigration(from = 66, to = 68, spec = AppDatabase.MIGRATION_66_68.class), // added event and moderationAction to NotificationEntity, new NotificationPolicyEntity + @AutoMigration(from = 68, to = 70), // added mastodonApiVersion to InstanceEntity } ) public abstract class AppDatabase extends RoomDatabase { diff --git a/app/src/main/java/com/keylesspalace/tusky/db/entity/InstanceEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/entity/InstanceEntity.kt index aac38217c..840bf1299 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/entity/InstanceEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/entity/InstanceEntity.kt @@ -42,6 +42,8 @@ data class InstanceEntity( val maxFieldNameLength: Int?, val maxFieldValueLength: Int?, val translationEnabled: Boolean?, + val mastodonApiVersion: Int?, + // ToDo: Remove this again when filter v1 support is dropped @ColumnInfo(defaultValue = "false") val filterV2Supported: Boolean = false ) @@ -69,4 +71,5 @@ data class InstanceInfoEntity( val maxFieldNameLength: Int?, val maxFieldValueLength: Int?, val translationEnabled: Boolean?, + val mastodonApiVersion: Int?, ) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Filter.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Filter.kt index f85be3834..bfbbee872 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Filter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Filter.kt @@ -9,39 +9,56 @@ import kotlinx.parcelize.Parcelize @JsonClass(generateAdapter = true) @Parcelize data class Filter( - val id: String, - val title: String, - val context: List, + val id: String = "", + val title: String = "", + val context: List, @Json(name = "expires_at") val expiresAt: Date? = null, - @Json(name = "filter_action") val filterAction: String, + @Json(name = "filter_action") val action: Action, // This field is mandatory according to the API documentation but is in fact optional in some instances val keywords: List = emptyList(), // val statuses: List, ) : Parcelable { enum class Action(val action: String) { + @Json(name = "none") NONE("none"), + + @Json(name = "blur") + BLUR("blur"), + + @Json(name = "warn") WARN("warn"), + + @Json(name = "hide") HIDE("hide"); + // Retrofit will call toString when sending this class as part of a form-urlencoded body. + override fun toString() = action + companion object { fun from(action: String): Action = entries.firstOrNull { it.action == action } ?: WARN } } enum class Kind(val kind: String) { + @Json(name = "home") HOME("home"), + + @Json(name = "notifications") NOTIFICATIONS("notifications"), + + @Json(name = "public") PUBLIC("public"), + + @Json(name = "thread") THREAD("thread"), + + @Json(name = "account") ACCOUNT("account"); + // Retrofit will call toString when sending this class as part of a form-urlencoded body. + override fun toString() = kind + companion object { fun from(kind: String): Kind = entries.firstOrNull { it.kind == kind } ?: PUBLIC } } - - val action: Action - get() = Action.from(filterAction) - - val kinds: List - get() = context.map { Kind.from(it) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/FilterV1.kt b/app/src/main/java/com/keylesspalace/tusky/entity/FilterV1.kt index 40f48e1cd..1636b8800 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/FilterV1.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/FilterV1.kt @@ -40,20 +40,18 @@ data class FilterV1( return other.id == id } - fun toFilter(): Filter { - return Filter( - id = id, - title = phrase, - context = context, - expiresAt = expiresAt, - filterAction = Filter.Action.WARN.action, - keywords = listOf( - FilterKeyword( - id = id, - keyword = phrase, - wholeWord = wholeWord - ) + fun toFilter() = Filter( + id = id, + title = phrase, + context = context.map(Filter.Kind::from), + expiresAt = expiresAt, + action = Filter.Action.WARN, + keywords = listOf( + FilterKeyword( + id = id, + keyword = phrase, + wholeWord = wholeWord ) ) - } + ) } diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt index 1fceb599e..f75ea34dc 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt @@ -17,7 +17,8 @@ data class Instance( // val registrations: Registrations, // val contact: Contact, val rules: List = emptyList(), - val pleroma: PleromaConfiguration? = null + val pleroma: PleromaConfiguration? = null, + @Json(name = "api_versions") val apiVersions: ApiVersions? = null, ) { @JsonClass(generateAdapter = true) data class Usage(val users: Users) { @@ -45,7 +46,7 @@ data class Instance( val statuses: Statuses? = null, @Json(name = "media_attachments") val mediaAttachments: MediaAttachments? = null, val polls: Polls? = null, - val translation: Translation? = null + val translation: Translation? = null, ) { @JsonClass(generateAdapter = true) data class Urls(@Json(name = "streaming_api") val streamingApi: String? = null) @@ -99,4 +100,7 @@ data class Instance( @JsonClass(generateAdapter = true) data class Rule(val id: String, val text: String) + + @JsonClass(generateAdapter = true) + data class ApiVersions(val mastodon: Int? = null) } diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt index 67b34e549..c4f2563bf 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt @@ -158,6 +158,12 @@ data class Status( return builder.toString() } + fun getApplicableFilter(kind: Filter.Kind): Filter? = + actionableStatus.filtered?.filter { it.filter.context.contains(kind) }?.maxByOrNull { it.filter.action.ordinal }?.filter + + fun shouldShowContent(alwayShowSensitiveContent: Boolean, context: Filter.Kind): Boolean = + alwayShowSensitiveContent || (!actionableStatus.sensitive && getApplicableFilter(context)?.action != Filter.Action.BLUR) + @JsonClass(generateAdapter = true) data class Mention( val id: String, diff --git a/app/src/main/java/com/keylesspalace/tusky/network/FilterModel.kt b/app/src/main/java/com/keylesspalace/tusky/network/FilterModel.kt index 355212589..86eefb834 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/FilterModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/FilterModel.kt @@ -66,13 +66,13 @@ class FilterModel @Inject constructor( ) } - fun shouldFilterStatus(status: Status): Filter.Action { + fun shouldFilterStatus(status: Status): Filter? { if (v1) { // Patterns are expensive and thread-safe, matchers are neither. - val matcher = pattern?.matcher("") ?: return Filter.Action.NONE + val matcher = pattern?.matcher("") ?: return null if (status.poll?.options?.any { matcher.reset(it.title).find() } == true) { - return Filter.Action.HIDE + return Filter(context = listOf(kind), action = Filter.Action.HIDE) } val spoilerText = status.actionableStatus.spoilerText @@ -83,21 +83,13 @@ class FilterModel @Inject constructor( (spoilerText.isNotEmpty() && matcher.reset(spoilerText).find()) || (attachmentsDescriptions.isNotEmpty() && matcher.reset(attachmentsDescriptions.joinToString("\n")).find()) ) { - Filter.Action.HIDE + return Filter(context = listOf(kind), action = Filter.Action.HIDE) } else { - Filter.Action.NONE + null } } - val matchingKind = status.filtered.orEmpty().filter { result -> - result.filter.kinds.contains(kind) - } - - return if (matchingKind.isEmpty()) { - Filter.Action.NONE - } else { - matchingKind.maxOf { it.filter.action } - } + return status.getApplicableFilter(kind) } private fun filterToRegexToken(filter: FilterV1): String? { 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 9b35c03dc..44e3c80bc 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -564,8 +564,8 @@ interface MastodonApi { @POST("api/v2/filters") suspend fun createFilter( @Field("title") title: String, - @Field("context[]") context: List, - @Field("filter_action") filterAction: String, + @Field("context[]") context: List, + @Field("filter_action") filterAction: Filter.Action, @Field("expires_in") expiresIn: FilterExpiration? ): NetworkResult @@ -574,8 +574,8 @@ interface MastodonApi { suspend fun updateFilter( @Path("id") id: String, @Field("title") title: String? = null, - @Field("context[]") context: List? = null, - @Field("filter_action") filterAction: String? = null, + @Field("context[]") context: List? = null, + @Field("filter_action") filterAction: Filter.Action? = null, @Field("expires_in") expires: FilterExpiration? = null ): NetworkResult diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.kt index 7c0f8adf2..7fc38bbd5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.kt @@ -36,6 +36,7 @@ package com.keylesspalace.tusky.util import androidx.paging.CombinedLoadStates import androidx.paging.LoadState +import com.keylesspalace.tusky.entity.Filter import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.entity.TrendingTag import com.keylesspalace.tusky.viewdata.StatusViewData @@ -47,17 +48,16 @@ fun Status.toViewData( isExpanded: Boolean, isCollapsed: Boolean, isDetailed: Boolean = false, + filter: Filter?, translation: TranslationViewData? = null, -): StatusViewData.Concrete { - return StatusViewData.Concrete( - status = this, - isShowingContent = isShowingContent, - isCollapsed = isCollapsed, - isExpanded = isExpanded, - isDetailed = isDetailed, - translation = translation, - ) -} +) = StatusViewData.Concrete( + status = this, + isShowingContent = isShowingContent, + isCollapsed = isCollapsed, + isExpanded = isExpanded, + isDetailed = isDetailed, + translation = translation, +).apply { this.filter = filter } fun List.toViewData(): List { val maxTrendingValue = flatMap { tag -> tag.history } diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt b/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt index 597832003..0751a9846 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt @@ -42,7 +42,7 @@ sealed interface TranslationViewData { */ sealed class StatusViewData { abstract val id: String - var filterAction: Filter.Action = Filter.Action.NONE + var filter: Filter? = null data class Concrete( val status: Status, diff --git a/app/src/main/res/layout/activity_edit_filter.xml b/app/src/main/res/layout/activity_edit_filter.xml index 08fed6c7a..7ea25b3e4 100644 --- a/app/src/main/res/layout/activity_edit_filter.xml +++ b/app/src/main/res/layout/activity_edit_filter.xml @@ -81,6 +81,13 @@ android:layout_width="match_parent" android:layout_height="wrap_content"> + + + @string/filter_action_blur @string/filter_action_warn @string/filter_action_hide diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d8deded4d..b70e40c92 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -817,9 +817,11 @@ My filter Title + Blur Warn Hide - Hide with a warning + Hide media with a warning + Hide entire post with a warning Hide completely Filter action Filter contexts diff --git a/app/src/test/java/com/keylesspalace/tusky/FilterV1Test.kt b/app/src/test/java/com/keylesspalace/tusky/FilterV1Test.kt index c7e9222c8..fe7a133ad 100644 --- a/app/src/test/java/com/keylesspalace/tusky/FilterV1Test.kt +++ b/app/src/test/java/com/keylesspalace/tusky/FilterV1Test.kt @@ -33,6 +33,7 @@ import java.util.Date import kotlinx.coroutines.runBlocking import okhttp3.ResponseBody.Companion.toResponseBody import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -127,8 +128,7 @@ class FilterV1Test { @Test fun shouldNotFilter() { - assertEquals( - Filter.Action.NONE, + assertNull( filterModel.shouldFilterStatus( mockStatus(content = "should not be filtered") ) @@ -141,7 +141,7 @@ class FilterV1Test { Filter.Action.HIDE, filterModel.shouldFilterStatus( mockStatus(content = "one two badWord three") - ) + )?.action ) } @@ -151,7 +151,7 @@ class FilterV1Test { Filter.Action.HIDE, filterModel.shouldFilterStatus( mockStatus(content = "one two badWordPart three") - ) + )?.action ) } @@ -161,14 +161,13 @@ class FilterV1Test { Filter.Action.HIDE, filterModel.shouldFilterStatus( mockStatus(content = "one two badWholeWord three") - ) + )?.action ) } @Test fun shouldNotFilter_whenContentDoesNotMatchWholeWord() { - assertEquals( - Filter.Action.NONE, + assertNull( filterModel.shouldFilterStatus( mockStatus(content = "one two badWholeWordTest three") ) @@ -184,7 +183,7 @@ class FilterV1Test { content = "should not be filtered", spoilerText = "badWord should be filtered" ) - ) + )?.action ) } @@ -198,7 +197,7 @@ class FilterV1Test { spoilerText = "should not be filtered", pollOptions = listOf("should not be filtered", "badWord") ) - ) + )?.action ) } @@ -212,7 +211,7 @@ class FilterV1Test { spoilerText = "should not be filtered", attachmentsDescriptions = listOf("should not be filtered", "badWord") ) - ) + )?.action ) } @@ -222,7 +221,7 @@ class FilterV1Test { Filter.Action.HIDE, filterModel.shouldFilterStatus( mockStatus(content = "one two someone@twitter.com three") - ) + )?.action ) } @@ -232,7 +231,7 @@ class FilterV1Test { Filter.Action.HIDE, filterModel.shouldFilterStatus( mockStatus(content = "#hashtag one two three") - ) + )?.action ) } @@ -242,14 +241,13 @@ class FilterV1Test { Filter.Action.HIDE, filterModel.shouldFilterStatus( mockStatus(content = "

#hashtagone two three

") - ) + )?.action ) } @Test fun shouldNotFilterHtmlAttributes() { - assertEquals( - Filter.Action.NONE, + assertNull( filterModel.shouldFilterStatus( mockStatus(content = "

https://foo.bar/ one two three

") ) @@ -258,8 +256,7 @@ class FilterV1Test { @Test fun shouldNotFilter_whenFilterIsExpired() { - assertEquals( - Filter.Action.NONE, + assertNull( filterModel.shouldFilterStatus( mockStatus(content = "content matching expired filter should not be filtered") ) @@ -272,7 +269,7 @@ class FilterV1Test { Filter.Action.HIDE, filterModel.shouldFilterStatus( mockStatus(content = "content matching unexpired filter should be filtered") - ) + )?.action ) } diff --git a/app/src/test/java/com/keylesspalace/tusky/components/compose/ComposeActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/components/compose/ComposeActivityTest.kt index d2a5bbf31..c25860e5f 100644 --- a/app/src/test/java/com/keylesspalace/tusky/components/compose/ComposeActivityTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/components/compose/ComposeActivityTest.kt @@ -137,7 +137,7 @@ class ComposeActivityTest { val instanceDaoMock: InstanceDao = mock { onBlocking { getInstanceInfo(any()) } doReturn - InstanceInfoEntity(instanceDomain, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null) + InstanceInfoEntity(instanceDomain, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,) onBlocking { getEmojiInfo(any()) } doReturn EmojisEntity(instanceDomain, emptyList()) } diff --git a/app/src/test/java/com/keylesspalace/tusky/components/timeline/CachedTimelineRemoteMediatorTest.kt b/app/src/test/java/com/keylesspalace/tusky/components/timeline/CachedTimelineRemoteMediatorTest.kt index d5bbd5fd7..64bbc9f63 100644 --- a/app/src/test/java/com/keylesspalace/tusky/components/timeline/CachedTimelineRemoteMediatorTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/components/timeline/CachedTimelineRemoteMediatorTest.kt @@ -12,6 +12,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.keylesspalace.tusky.components.timeline.viewmodel.CachedTimelineRemoteMediator import com.keylesspalace.tusky.components.timeline.viewmodel.CachedTimelineViewModel +import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.db.Converters @@ -531,6 +532,7 @@ class CachedTimelineRemoteMediatorTest { return mock { on { accountManager } doReturn accManager on { activeAccountFlow } doReturn MutableStateFlow(account) + on { kind } doReturn TimelineViewModel.Kind.PUBLIC_FEDERATED } } } diff --git a/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelineRemoteMediatorTest.kt b/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelineRemoteMediatorTest.kt index 459a9deb7..c82dc19ef 100644 --- a/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelineRemoteMediatorTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelineRemoteMediatorTest.kt @@ -94,6 +94,8 @@ class NetworkTimelineRemoteMediatorTest { on { activeAccountFlow } doReturn MutableStateFlow(account) on { statusData } doReturn statuses on { nextKey } doReturn null + on { kind } doReturn TimelineViewModel.Kind.PUBLIC_FEDERATED + onBlocking { fetchStatusesForKind(null, null, 20) } doReturn Response.success( listOf( fakeStatus("7"), @@ -147,6 +149,8 @@ class NetworkTimelineRemoteMediatorTest { on { activeAccountFlow } doReturn MutableStateFlow(account) on { statusData } doReturn statuses on { nextKey } doReturn "0" + on { kind } doReturn TimelineViewModel.Kind.PUBLIC_FEDERATED + onBlocking { fetchStatusesForKind(null, null, 20) } doReturn Response.success( listOf( fakeStatus("5"), @@ -201,6 +205,8 @@ class NetworkTimelineRemoteMediatorTest { on { activeAccountFlow } doReturn MutableStateFlow(account) on { statusData } doReturn statuses on { nextKey } doReturn "0" + on { kind } doReturn TimelineViewModel.Kind.PUBLIC_FEDERATED + onBlocking { fetchStatusesForKind(null, null, 20) } doReturn Response.success( listOf( fakeStatus("10"), @@ -256,6 +262,8 @@ class NetworkTimelineRemoteMediatorTest { on { activeAccountFlow } doReturn MutableStateFlow(account) on { statusData } doReturn statuses on { nextKey } doReturn "3" + on { kind } doReturn TimelineViewModel.Kind.PUBLIC_FEDERATED + onBlocking { fetchStatusesForKind("3", null, 20) } doReturn Response.success( listOf( fakeStatus("3"), @@ -311,6 +319,8 @@ class NetworkTimelineRemoteMediatorTest { on { activeAccountFlow } doReturn MutableStateFlow(account) on { statusData } doReturn statuses on { nextKey } doReturn "3" + on { kind } doReturn TimelineViewModel.Kind.PUBLIC_FEDERATED + onBlocking { fetchStatusesForKind("3", null, 20) } doReturn Response.success( listOf( fakeStatus("3"), From cf3aae360e6199c51755fa0bc2123ccdf35e7e78 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Tue, 29 Apr 2025 19:38:35 +0200 Subject: [PATCH 46/54] Improve poll voting & media label ui (#5047) - easier to read - easier to see where one text starts and the other one ends especially when multiline - larger clickable areas Before After --- .../tusky/adapter/PollAdapter.kt | 48 ++++-- .../tusky/adapter/StatusBaseViewHolder.java | 17 +- .../tusky/util/StatusViewHelper.kt | 3 +- .../tusky/viewdata/PollViewData.kt | 17 +- .../res/drawable/ic_check_circle_24dp.xml | 10 ++ .../main/res/layout/item_media_preview.xml | 156 ++++++++++++------ app/src/main/res/layout/item_poll.xml | 85 ++++++---- app/src/main/res/layout/item_status.xml | 21 +-- 8 files changed, 237 insertions(+), 120 deletions(-) create mode 100644 app/src/main/res/drawable/ic_check_circle_24dp.xml diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt index aac3c8159..249284bb1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt @@ -15,10 +15,12 @@ package com.keylesspalace.tusky.adapter +import android.content.res.ColorStateList import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.color.MaterialColors import com.keylesspalace.tusky.R import com.keylesspalace.tusky.databinding.ItemPollBinding import com.keylesspalace.tusky.entity.Emoji @@ -91,24 +93,34 @@ class PollAdapter : RecyclerView.Adapter>() { radioButton.isEnabled = enabled checkBox.isEnabled = enabled - when (mode) { - RESULT -> { - val percent = calculatePercent(option.votesCount, votersCount, voteCount) - resultTextView.text = buildDescription(option.title, percent, option.voted, resultTextView.context) - .emojify(emojis, resultTextView, animateEmojis) - - val level = percent * 100 - val optionColor = if (option.voted) { - R.color.colorBackgroundHighlight - } else { - R.color.colorBackgroundAccent - } + if (mode == RESULT) { + val percent = calculatePercent(option.votesCount, votersCount, voteCount) + resultTextView.text = buildDescription(option.title, percent, option.voted, resultTextView.context, resultTextView) + .emojify(emojis, resultTextView, animateEmojis) + + val level = percent * 100 + val optionColor = if (option.voted) { + R.color.colorBackgroundHighlight + } else { + R.color.colorBackgroundAccent + } - resultTextView.background.level = level - resultTextView.background.setTint(resultTextView.context.getColor(optionColor)) - resultTextView.setOnClickListener(resultClickListener) + holder.binding.pollLayout.setBackgroundResource(R.drawable.poll_option_background) + holder.binding.pollLayout.background.level = level + holder.binding.pollLayout.background.setTint(resultTextView.context.getColor(optionColor)) + holder.binding.root.strokeColor = holder.binding.root.context.getColor(optionColor) + resultTextView.setOnClickListener(resultClickListener) + } else { + holder.binding.pollLayout.background = null + + if (option.selected) { + holder.binding.root.setCardBackgroundColor(ColorStateList.valueOf(MaterialColors.getColor(holder.binding.root, com.google.android.material.R.attr.colorSurface))) + holder.binding.root.strokeColor = MaterialColors.getColor(holder.binding.root, com.google.android.material.R.attr.colorSurface) + } else { + holder.binding.root.setCardBackgroundColor(ColorStateList.valueOf(MaterialColors.getColor(holder.binding.root, android.R.attr.colorBackground))) + holder.binding.root.strokeColor = MaterialColors.getColor(holder.binding.root, R.attr.colorBackgroundAccent) } - SINGLE -> { + if (mode == SINGLE) { radioButton.text = option.title.emojify(emojis, radioButton, animateEmojis) radioButton.isChecked = option.selected radioButton.setOnClickListener { @@ -117,12 +129,12 @@ class PollAdapter : RecyclerView.Adapter>() { notifyItemChanged(index) } } - } - MULTIPLE -> { + } else { // mode == MULTIPLE checkBox.text = option.title.emojify(emojis, checkBox, animateEmojis) checkBox.isChecked = option.selected checkBox.setOnCheckedChangeListener { _, isChecked -> pollOptions[holder.bindingAdapterPosition].selected = isChecked + notifyItemChanged(holder.bindingAdapterPosition) } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java index b28bcc702..fe5de1467 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java @@ -103,6 +103,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { private final TextView sensitiveMediaWarning; private final View sensitiveMediaShow; protected final TextView[] mediaLabels; + protected final MaterialCardView[] mediaLabelContainers; protected final CharSequence[] mediaDescriptions; private final MaterialButton contentWarningButton; private final ImageView avatarInset; @@ -167,6 +168,12 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { itemView.findViewById(R.id.status_media_label_2), itemView.findViewById(R.id.status_media_label_3) }; + mediaLabelContainers = new MaterialCardView[]{ + itemView.findViewById(R.id.status_media_label_container_0), + itemView.findViewById(R.id.status_media_label_container_1), + itemView.findViewById(R.id.status_media_label_container_2), + itemView.findViewById(R.id.status_media_label_container_3) + }; mediaDescriptions = new CharSequence[mediaLabels.length]; contentWarningDescription = itemView.findViewById(R.id.status_content_warning_description); contentWarningButton = itemView.findViewById(R.id.status_content_warning_button); @@ -621,7 +628,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { TextView mediaLabel = mediaLabels[i]; if (i < attachments.size()) { Attachment attachment = attachments.get(i); - mediaLabel.setVisibility(View.VISIBLE); + mediaLabelContainers[i].setVisibility(View.VISIBLE); mediaDescriptions[i] = AttachmentHelper.getFormattedDescription(attachment, context); updateMediaLabel(i, sensitive, showingContent); @@ -631,7 +638,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { setAttachmentClickListener(mediaLabel, listener, i, mediaDescriptions[i], false); } else { - mediaLabel.setVisibility(View.GONE); + mediaLabelContainers[i].setVisibility(View.GONE); } } } @@ -822,8 +829,8 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { hideSensitiveMediaWarning(); } // Hide the unused label. - for (TextView mediaLabel : mediaLabels) { - mediaLabel.setVisibility(View.GONE); + for (MaterialCardView mediaLabelContainer : mediaLabelContainers) { + mediaLabelContainer.setVisibility(View.GONE); } } else { mediaContainer.setVisibility(View.VISIBLE); @@ -1034,7 +1041,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { for (int i = 0; i < args.length; i++) { if (i < options.size()) { int percent = PollViewDataKt.calculatePercent(options.get(i).getVotesCount(), poll.getVotersCount(), poll.getVotesCount()); - args[i] = buildDescription(options.get(i).getTitle(), percent, options.get(i).getVoted(), context); + args[i] = buildDescription(options.get(i).getTitle(), percent, options.get(i).getVoted(), context, null); } else { args[i] = ""; } diff --git a/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt index d73d9e87f..fdf848023 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt @@ -348,7 +348,8 @@ class StatusViewHelper(private val itemView: View) { options[i].title, percent, options[i].voted, - pollResults[i].context + pollResults[i].context, + pollResults[i] ) pollResults[i].text = pollOptionText.emojify(emojis, pollResults[i], animateEmojis) pollResults[i].visibility = View.VISIBLE diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/PollViewData.kt b/app/src/main/java/com/keylesspalace/tusky/viewdata/PollViewData.kt index 07d95982e..40995804d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewdata/PollViewData.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/PollViewData.kt @@ -16,9 +16,16 @@ package com.keylesspalace.tusky.viewdata import android.content.Context +import android.text.Spannable import android.text.SpannableStringBuilder import android.text.Spanned +import android.text.style.DynamicDrawableSpan +import android.text.style.ImageSpan +import android.widget.TextView +import androidx.appcompat.content.res.AppCompatResources import androidx.core.text.parseAsHtml +import androidx.core.text.set +import com.google.android.material.color.MaterialColors import com.keylesspalace.tusky.R import com.keylesspalace.tusky.entity.Poll import com.keylesspalace.tusky.entity.PollOption @@ -52,13 +59,21 @@ fun calculatePercent(fraction: Int?, totalVoters: Int?, totalVotes: Int): Int { } } -fun buildDescription(title: String, percent: Int, voted: Boolean, context: Context): Spanned { +fun buildDescription(title: String, percent: Int, voted: Boolean, context: Context, textView: TextView? = null): Spanned { val builder = SpannableStringBuilder( context.getString(R.string.poll_percent_format, percent).parseAsHtml() ) if (voted) { builder.append(" ✓ ") + + if (textView != null) { + val size = (textView.textSize * 1.1).toInt() + val drawable = AppCompatResources.getDrawable(context, R.drawable.ic_check_circle_24dp)!! + drawable.setBounds(0, 0, size, size) + drawable.setTint(MaterialColors.getColor(textView, android.R.attr.textColorPrimary)) + builder.setSpan(ImageSpan(drawable, DynamicDrawableSpan.ALIGN_CENTER), builder.length - 2, builder.length - 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } } else { builder.append(" ") } diff --git a/app/src/main/res/drawable/ic_check_circle_24dp.xml b/app/src/main/res/drawable/ic_check_circle_24dp.xml new file mode 100644 index 000000000..728be3ba6 --- /dev/null +++ b/app/src/main/res/drawable/ic_check_circle_24dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/item_media_preview.xml b/app/src/main/res/layout/item_media_preview.xml index d6dd8181a..f4ef7e91a 100644 --- a/app/src/main/res/layout/item_media_preview.xml +++ b/app/src/main/res/layout/item_media_preview.xml @@ -47,68 +47,120 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> - + app:layout_constraintTop_toTopOf="parent"> - + + + + + app:layout_constraintTop_toBottomOf="@id/status_media_label_container_0"> - + + + + + app:layout_constraintTop_toBottomOf="@id/status_media_label_container_1"> - + + + + + app:layout_constraintTop_toBottomOf="@id/status_media_label_container_2"> + + + + diff --git a/app/src/main/res/layout/item_poll.xml b/app/src/main/res/layout/item_poll.xml index fdc0614a7..25504dbe8 100644 --- a/app/src/main/res/layout/item_poll.xml +++ b/app/src/main/res/layout/item_poll.xml @@ -1,43 +1,62 @@ - + android:layout_height="wrap_content" + android:layout_marginTop="6dp" + app:cardCornerRadius="8dp" + app:cardBackgroundColor="?android:attr/colorBackground" + app:strokeColor="?attr/colorBackgroundAccent"> - + android:paddingStart="4dp" + android:paddingTop="4dp" + android:paddingEnd="4dp" + android:paddingBottom="4dp"> - + - + + + - + + diff --git a/app/src/main/res/layout/item_status.xml b/app/src/main/res/layout/item_status.xml index 9767905c2..17f123a1d 100644 --- a/app/src/main/res/layout/item_status.xml +++ b/app/src/main/res/layout/item_status.xml @@ -84,8 +84,8 @@ android:ellipsize="end" android:importantForAccessibility="no" android:maxLines="1" - android:paddingEnd="@dimen/status_display_name_padding_end" android:paddingStart="@dimen/status_display_name_padding_end" + android:paddingEnd="@dimen/status_display_name_padding_end" android:textColor="?android:textColorSecondary" android:textSize="?attr/status_text_medium" app:layout_constraintEnd_toStartOf="@id/status_meta_info" @@ -256,37 +256,38 @@