From 45cc000d07162b40748771ae96a2ed502fc405eb Mon Sep 17 00:00:00 2001 From: kyori19 Date: Sun, 13 Nov 2022 06:05:55 +0900 Subject: [PATCH 001/232] Add or remove from lists in AccountActivity --- .../components/account/AccountActivity.kt | 9 + .../account/list/ListsForAccountFragment.kt | 206 ++++++++++++++++++ .../account/list/ListsForAccountViewModel.kt | 137 ++++++++++++ .../tusky/di/FragmentBuildersModule.kt | 4 + .../tusky/di/ViewModelFactory.kt | 6 + .../tusky/network/MastodonApi.kt | 5 + .../res/layout/fragment_lists_for_account.xml | 40 ++++ .../layout/item_add_or_remove_from_list.xml | 49 +++++ app/src/main/res/menu/account_toolbar.xml | 6 +- app/src/main/res/values/strings.xml | 4 + 10 files changed, 465 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/account/list/ListsForAccountFragment.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/account/list/ListsForAccountViewModel.kt create mode 100644 app/src/main/res/layout/fragment_lists_for_account.xml create mode 100644 app/src/main/res/layout/item_add_or_remove_from_list.xml diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt index 6c8f997b..b66ebdb3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt @@ -53,6 +53,7 @@ import com.keylesspalace.tusky.EditProfileActivity import com.keylesspalace.tusky.R import com.keylesspalace.tusky.StatusListActivity import com.keylesspalace.tusky.ViewMediaActivity +import com.keylesspalace.tusky.components.account.list.ListsForAccountFragment import com.keylesspalace.tusky.components.compose.ComposeActivity import com.keylesspalace.tusky.components.report.ReportActivity import com.keylesspalace.tusky.databinding.ActivityAccountBinding @@ -740,6 +741,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI menu.removeItem(R.id.action_report) } + if (!viewModel.isSelf && followState != FollowState.FOLLOWING) { + menu.removeItem(R.id.action_add_or_remove_from_list) + } + return super.onCreateOptionsMenu(menu) } @@ -852,6 +857,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI toggleMute() return true } + R.id.action_add_or_remove_from_list -> { + ListsForAccountFragment.newInstance(viewModel.accountId).show(supportFragmentManager, null) + return true + } R.id.action_mute_domain -> { toggleBlockDomain(domain) return true diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/list/ListsForAccountFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/list/ListsForAccountFragment.kt new file mode 100644 index 00000000..d4fb7c75 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/list/ListsForAccountFragment.kt @@ -0,0 +1,206 @@ +/* Copyright 2022 kyori19 + * + * 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.account.list + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.ListAdapter +import com.google.android.material.snackbar.Snackbar +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.FragmentListsForAccountBinding +import com.keylesspalace.tusky.databinding.ItemAddOrRemoveFromListBinding +import com.keylesspalace.tusky.di.Injectable +import com.keylesspalace.tusky.di.ViewModelFactory +import com.keylesspalace.tusky.util.BindingHolder +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.viewBinding +import com.keylesspalace.tusky.util.visible +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import java.io.IOException +import javax.inject.Inject + +class ListsForAccountFragment : DialogFragment(), Injectable { + + @Inject + lateinit var viewModelFactory: ViewModelFactory + + private val viewModel: ListsForAccountViewModel by viewModels { viewModelFactory } + private val binding by viewBinding(FragmentListsForAccountBinding::bind) + + private val adapter = Adapter() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(STYLE_NORMAL, R.style.TuskyDialogFragmentStyle) + + viewModel.setup(requireArguments().getString(ARG_ACCOUNT_ID)!!) + } + + override fun onStart() { + super.onStart() + dialog?.apply { + window?.setLayout( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.MATCH_PARENT, + ) + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_lists_for_account, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + binding.listsView.layoutManager = LinearLayoutManager(view.context) + binding.listsView.adapter = adapter + + viewLifecycleOwner.lifecycleScope.launch { + viewModel.states.collectLatest { states -> + binding.progressBar.hide() + if (states.isEmpty()) { + binding.messageView.show() + binding.messageView.setup(R.drawable.elephant_friend_empty, R.string.no_lists) { + load() + } + } else { + binding.listsView.show() + adapter.submitList(states) + } + } + } + + viewLifecycleOwner.lifecycleScope.launch { + viewModel.loadError.collectLatest { error -> + binding.progressBar.hide() + binding.listsView.hide() + binding.messageView.apply { + show() + + if (error is IOException) { + setup(R.drawable.elephant_offline, R.string.error_network) { + load() + } + } else { + setup(R.drawable.elephant_error, R.string.error_generic) { + load() + } + } + } + } + } + + viewLifecycleOwner.lifecycleScope.launch { + viewModel.actionError.collectLatest { error -> + when (error.type) { + ActionError.Type.ADD -> { + Snackbar.make(binding.root, R.string.failed_to_add_to_list, Snackbar.LENGTH_LONG) + .setAction(R.string.action_retry) { + viewModel.addAccountToList(error.listId) + } + .show() + } + ActionError.Type.REMOVE -> { + Snackbar.make(binding.root, R.string.failed_to_remove_from_list, Snackbar.LENGTH_LONG) + .setAction(R.string.action_retry) { + viewModel.removeAccountFromList(error.listId) + } + .show() + } + } + } + } + + load() + } + + private fun load() { + binding.progressBar.show() + binding.listsView.hide() + binding.messageView.hide() + viewModel.load() + } + + private object Differ : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: AccountListState, + newItem: AccountListState + ): Boolean { + return oldItem.list.id == newItem.list.id + } + + override fun areContentsTheSame( + oldItem: AccountListState, + newItem: AccountListState + ): Boolean { + return oldItem == newItem + } + } + + inner class Adapter : + ListAdapter>(Differ) { + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int, + ): BindingHolder { + val binding = + ItemAddOrRemoveFromListBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return BindingHolder(binding) + } + + override fun onBindViewHolder(holder: BindingHolder, position: Int) { + val item = getItem(position) + holder.binding.listNameView.text = item.list.title + holder.binding.addButton.apply { + visible(!item.includesAccount) + setOnClickListener { + viewModel.addAccountToList(item.list.id) + } + } + holder.binding.removeButton.apply { + visible(item.includesAccount) + setOnClickListener { + viewModel.removeAccountFromList(item.list.id) + } + } + } + } + + companion object { + private const val ARG_ACCOUNT_ID = "accountId" + + fun newInstance(accountId: String): ListsForAccountFragment { + val args = Bundle().apply { + putString(ARG_ACCOUNT_ID, accountId) + } + return ListsForAccountFragment().apply { arguments = args } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/list/ListsForAccountViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/list/ListsForAccountViewModel.kt new file mode 100644 index 00000000..b571390e --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/list/ListsForAccountViewModel.kt @@ -0,0 +1,137 @@ +/* Copyright 2022 kyori19 + * + * 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.account.list + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import at.connyduck.calladapter.networkresult.getOrThrow +import at.connyduck.calladapter.networkresult.onFailure +import at.connyduck.calladapter.networkresult.onSuccess +import at.connyduck.calladapter.networkresult.runCatching +import com.keylesspalace.tusky.entity.MastoList +import com.keylesspalace.tusky.network.MastodonApi +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import javax.inject.Inject + +data class AccountListState( + val list: MastoList, + val includesAccount: Boolean, +) + +data class ActionError( + val error: Throwable, + val type: Type, + val listId: String, +) : Throwable(error) { + enum class Type { + ADD, + REMOVE, + } +} + +@OptIn(ExperimentalCoroutinesApi::class) +class ListsForAccountViewModel @Inject constructor( + private val mastodonApi: MastodonApi, +) : ViewModel() { + + private lateinit var accountId: String + + private val _states = MutableSharedFlow>(1) + val states: SharedFlow> = _states + + private val _loadError = MutableSharedFlow(1) + val loadError: SharedFlow = _loadError + + private val _actionError = MutableSharedFlow(1) + val actionError: SharedFlow = _actionError + + fun setup(accountId: String) { + this.accountId = accountId + } + + fun load() { + _loadError.resetReplayCache() + viewModelScope.launch { + runCatching { + val (all, includes) = listOf( + async { mastodonApi.getLists() }, + async { mastodonApi.getListsIncludesAccount(accountId) }, + ).awaitAll() + + _states.emit( + all.getOrThrow().map { list -> + AccountListState( + list = list, + includesAccount = includes.getOrThrow().any { it.id == list.id }, + ) + } + ) + } + .onFailure { + _loadError.emit(it) + } + } + } + + fun addAccountToList(listId: String) { + _actionError.resetReplayCache() + viewModelScope.launch { + mastodonApi.addAccountToList(listId, listOf(accountId)) + .onSuccess { + _states.emit( + _states.first().map { state -> + if (state.list.id == listId) { + state.copy(includesAccount = true) + } else { + state + } + } + ) + } + .onFailure { + _actionError.emit(ActionError(it, ActionError.Type.ADD, listId)) + } + } + } + + fun removeAccountFromList(listId: String) { + _actionError.resetReplayCache() + viewModelScope.launch { + mastodonApi.deleteAccountFromList(listId, listOf(accountId)) + .onSuccess { + _states.emit( + _states.first().map { state -> + if (state.list.id == listId) { + state.copy(includesAccount = false) + } else { + state + } + } + ) + } + .onFailure { + _actionError.emit(ActionError(it, ActionError.Type.REMOVE, listId)) + } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/di/FragmentBuildersModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/FragmentBuildersModule.kt index 989fb526..573689de 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/FragmentBuildersModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/FragmentBuildersModule.kt @@ -16,6 +16,7 @@ package com.keylesspalace.tusky.di import com.keylesspalace.tusky.AccountsInListFragment +import com.keylesspalace.tusky.components.account.list.ListsForAccountFragment import com.keylesspalace.tusky.components.account.media.AccountMediaFragment import com.keylesspalace.tusky.components.conversation.ConversationsFragment import com.keylesspalace.tusky.components.instancemute.fragment.InstanceListFragment @@ -91,4 +92,7 @@ abstract class FragmentBuildersModule { @ContributesAndroidInjector abstract fun preferencesFragment(): PreferencesFragment + + @ContributesAndroidInjector + abstract fun listsForAccountFragment(): ListsForAccountFragment } diff --git a/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt b/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt index 29aa3b47..bf697b40 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt @@ -5,6 +5,7 @@ package com.keylesspalace.tusky.di import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.keylesspalace.tusky.components.account.AccountViewModel +import com.keylesspalace.tusky.components.account.list.ListsForAccountViewModel import com.keylesspalace.tusky.components.account.media.AccountMediaViewModel import com.keylesspalace.tusky.components.announcements.AnnouncementsViewModel import com.keylesspalace.tusky.components.compose.ComposeViewModel @@ -126,5 +127,10 @@ abstract class ViewModelModule { @ViewModelKey(LoginWebViewViewModel::class) internal abstract fun loginWebViewViewModel(viewModel: LoginWebViewViewModel): ViewModel + @Binds + @IntoMap + @ViewModelKey(ListsForAccountViewModel::class) + internal abstract fun listsForAccountViewModel(viewModel: ListsForAccountViewModel): ViewModel + // Add more ViewModels here } 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 f9272994..08c2ed96 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -481,6 +481,11 @@ interface MastodonApi { @GET("/api/v1/lists") suspend fun getLists(): NetworkResult> + @GET("/api/v1/accounts/{id}/lists") + suspend fun getListsIncludesAccount( + @Path("id") accountId: String + ): NetworkResult> + @FormUrlEncoded @POST("api/v1/lists") suspend fun createList( diff --git a/app/src/main/res/layout/fragment_lists_for_account.xml b/app/src/main/res/layout/fragment_lists_for_account.xml new file mode 100644 index 00000000..3882141b --- /dev/null +++ b/app/src/main/res/layout/fragment_lists_for_account.xml @@ -0,0 +1,40 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/item_add_or_remove_from_list.xml b/app/src/main/res/layout/item_add_or_remove_from_list.xml new file mode 100644 index 00000000..ceb8bbaf --- /dev/null +++ b/app/src/main/res/layout/item_add_or_remove_from_list.xml @@ -0,0 +1,49 @@ + + + + + + + + + + diff --git a/app/src/main/res/menu/account_toolbar.xml b/app/src/main/res/menu/account_toolbar.xml index 8e0dc242..bdcbc940 100644 --- a/app/src/main/res/menu/account_toolbar.xml +++ b/app/src/main/res/menu/account_toolbar.xml @@ -18,6 +18,10 @@ android:title="@string/action_block" app:showAsAction="never" /> + + @@ -29,4 +33,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d27a9514..ac8237d4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -404,6 +404,9 @@ Search for people you follow Add account to the list Remove account from the list + Add or remove from list + Failed to add the account to the list + Failed to remove the account from the list Posting as %1$s @@ -624,6 +627,7 @@ You don\'t have any drafts. You don\'t have any scheduled posts. There are no announcements. + You don\'t have any lists. Mastodon has a minimum scheduling interval of 5 minutes. Show username in toolbars Show link previews in timelines From c00c0926cfff702bd535943685cfed0ebbe335a6 Mon Sep 17 00:00:00 2001 From: kyori19 Date: Wed, 16 Nov 2022 20:55:36 +0900 Subject: [PATCH 002/232] Update dialog colors --- app/src/main/res/layout/item_add_or_remove_from_list.xml | 9 +++++---- app/src/main/res/values/styles.xml | 1 - 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/res/layout/item_add_or_remove_from_list.xml b/app/src/main/res/layout/item_add_or_remove_from_list.xml index ceb8bbaf..732a883e 100644 --- a/app/src/main/res/layout/item_add_or_remove_from_list.xml +++ b/app/src/main/res/layout/item_add_or_remove_from_list.xml @@ -4,10 +4,10 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:paddingHorizontal="16dp" - android:paddingVertical="8dp" android:gravity="center_vertical" - android:orientation="horizontal"> + android:orientation="horizontal" + android:paddingHorizontal="16dp" + android:paddingVertical="8dp"> 8dp - @color/colorBackground + + From 64a06bfbe25ff311daf1dab5d7b2385717006f1b Mon Sep 17 00:00:00 2001 From: Nik Clayton Date: Mon, 5 Dec 2022 14:33:38 +0100 Subject: [PATCH 015/232] Support a swipe down to dismiss video (#2879) * Support a swipe down to dismiss video Images can already be dismissed with a swipe, this adds the same functionality to videos. - Add a VideoActionsListener interface for the hosting activity to dismiss the fragment - Add a gesture listener for swipes - Dismiss the fragment if a swipe has a greated Y component than X Fixes https://github.com/tuskyapp/Tusky/issues/2833 * Scale the video view when dragging Provides identical visual feedback to the same operation on images. --- .../keylesspalace/tusky/ViewMediaActivity.kt | 3 +- .../tusky/fragment/ViewVideoFragment.kt | 63 +++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt b/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt index 344ca3ac..8c7dff59 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt @@ -52,6 +52,7 @@ import com.keylesspalace.tusky.components.viewthread.ViewThreadActivity import com.keylesspalace.tusky.databinding.ActivityViewMediaBinding import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.fragment.ViewImageFragment +import com.keylesspalace.tusky.fragment.ViewVideoFragment import com.keylesspalace.tusky.pager.ImagePagerAdapter import com.keylesspalace.tusky.pager.SingleImagePagerAdapter import com.keylesspalace.tusky.util.getTemporaryMediaFilename @@ -68,7 +69,7 @@ import java.util.Locale typealias ToolbarVisibilityListener = (isVisible: Boolean) -> Unit -class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener { +class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener, ViewVideoFragment.VideoActionsListener { private val binding by viewBinding(ActivityViewMediaBinding::inflate) diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt index 214741a8..28991c26 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt @@ -18,27 +18,36 @@ package com.keylesspalace.tusky.fragment import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.annotation.SuppressLint +import android.content.Context import android.os.Bundle import android.os.Handler import android.os.Looper import android.text.method.ScrollingMovementMethod +import android.view.GestureDetector import android.view.KeyEvent import android.view.LayoutInflater +import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.widget.MediaController +import androidx.core.view.GestureDetectorCompat import com.keylesspalace.tusky.ViewMediaActivity import com.keylesspalace.tusky.databinding.FragmentViewVideoBinding import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.visible import com.keylesspalace.tusky.view.ExposedPlayPauseVideoView +import kotlin.math.abs class ViewVideoFragment : ViewMediaFragment() { + interface VideoActionsListener { + fun onDismiss() + } private var _binding: FragmentViewVideoBinding? = null private val binding get() = _binding!! + private lateinit var videoActionsListener: VideoActionsListener private lateinit var toolbar: View private val handler = Handler(Looper.getMainLooper()) private val hideToolbar = Runnable { @@ -52,6 +61,11 @@ class ViewVideoFragment : ViewMediaFragment() { private lateinit var mediaController: MediaController private var isAudio = false + override fun onAttach(context: Context) { + super.onAttach(context) + videoActionsListener = context as VideoActionsListener + } + override fun setUserVisibleHint(isVisibleToUser: Boolean) { // Start/pause/resume video playback as fragment is shown/hidden super.setUserVisibleHint(isVisibleToUser) @@ -168,6 +182,7 @@ class ViewVideoFragment : ViewMediaFragment() { return binding.root } + @SuppressLint("ClickableViewAccessibility") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val attachment = arguments?.getParcelable(ARG_ATTACHMENT) @@ -177,6 +192,54 @@ class ViewVideoFragment : ViewMediaFragment() { } val url = attachment.url isAudio = attachment.type == Attachment.Type.AUDIO + + val gestureDetector = GestureDetectorCompat( + requireContext(), + object : GestureDetector.SimpleOnGestureListener() { + override fun onDown(event: MotionEvent): Boolean { + return true + } + + override fun onFling( + e1: MotionEvent, + e2: MotionEvent, + velocityX: Float, + velocityY: Float + ): Boolean { + if (abs(velocityY) > abs(velocityX)) { + videoActionsListener.onDismiss() + return true + } + return false + } + } + ) + + var lastY = 0f + binding.root.setOnTouchListener { _, event -> + if (event.action == MotionEvent.ACTION_DOWN) { + lastY = event.rawY + } else if (event.pointerCount == 1 && event.action == MotionEvent.ACTION_MOVE) { + val diff = event.rawY - lastY + if (binding.videoView.translationY != 0f || abs(diff) > 40) { + binding.videoView.translationY += diff + val scale = (-abs(binding.videoView.translationY) / 720 + 1).coerceAtLeast(0.5f) + binding.videoView.scaleY = scale + binding.videoView.scaleX = scale + lastY = event.rawY + return@setOnTouchListener true + } + } else if (event.action == MotionEvent.ACTION_UP || event.action == MotionEvent.ACTION_CANCEL) { + if (abs(binding.videoView.translationY) > 180) { + videoActionsListener.onDismiss() + } else { + binding.videoView.animate().translationY(0f).scaleX(1f).scaleY(1f).start() + } + } + + gestureDetector.onTouchEvent(event) + } + finalizeViewSetup(url, attachment.previewUrl, attachment.description) } From 424326f99fca6492e8923499b67189222ea5d373 Mon Sep 17 00:00:00 2001 From: Nik Clayton Date: Mon, 5 Dec 2022 14:36:30 +0100 Subject: [PATCH 016/232] Add a menu option to mute / filter a hashtag from a status list (#2882) * Add a menu option to mute / filter a hashtag from a status list Un/muting uses the "home" filter * Set the initial mute button visibility from existing filters Check the user's filters to see if the tag is already filtered from HOME. If it is then the initial button is to unmute it. If it isn't then the initial button is to mute it. * Avoid "mute tag" menu items "popping" in - Initial state shows the "mute" option, disabled - Update the state after the API call completes --- .../keylesspalace/tusky/StatusListActivity.kt | 100 ++++++++++++++++++ .../main/res/menu/view_hashtag_toolbar.xml | 13 +++ app/src/main/res/values/strings.xml | 2 + 3 files changed, 115 insertions(+) diff --git a/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt b/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt index cc12479a..917416b4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt @@ -22,15 +22,22 @@ import android.util.Log import android.view.Menu import android.view.MenuItem import androidx.fragment.app.commit +import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import at.connyduck.calladapter.networkresult.fold +import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider +import autodispose2.autoDispose import com.google.android.material.snackbar.Snackbar +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.appstore.PreferenceChangedEvent import com.keylesspalace.tusky.components.timeline.TimelineFragment import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel.Kind import com.keylesspalace.tusky.databinding.ActivityStatuslistBinding +import com.keylesspalace.tusky.entity.Filter import com.keylesspalace.tusky.util.viewBinding import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import kotlinx.coroutines.launch import javax.inject.Inject @@ -39,13 +46,22 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector { @Inject lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector + @Inject + lateinit var eventHub: EventHub + private val binding: ActivityStatuslistBinding by viewBinding(ActivityStatuslistBinding::inflate) private lateinit var kind: Kind private var hashtag: String? = null private var followTagItem: MenuItem? = null private var unfollowTagItem: MenuItem? = null + private var muteTagItem: MenuItem? = null + private var unmuteTagItem: MenuItem? = null + + /** The filter muting hashtag, null if unknown or hashtag is not filtered */ + private var mutedFilter: Filter? = null override fun onCreate(savedInstanceState: Bundle?) { + Log.d("StatusListActivity", "onCreate") super.onCreate(savedInstanceState) setContentView(binding.root) @@ -89,10 +105,15 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector { menuInflater.inflate(R.menu.view_hashtag_toolbar, menu) followTagItem = menu.findItem(R.id.action_follow_hashtag) unfollowTagItem = menu.findItem(R.id.action_unfollow_hashtag) + muteTagItem = menu.findItem(R.id.action_mute_hashtag) + unmuteTagItem = menu.findItem(R.id.action_unmute_hashtag) followTagItem?.isVisible = tagEntity.following == false unfollowTagItem?.isVisible = tagEntity.following == true followTagItem?.setOnMenuItemClickListener { followTag() } unfollowTagItem?.setOnMenuItemClickListener { unfollowTag() } + muteTagItem?.setOnMenuItemClickListener { muteTag() } + unmuteTagItem?.setOnMenuItemClickListener { unmuteTag() } + updateMuteTagMenuItems() }, { Log.w(TAG, "Failed to query tag #$tag", it) @@ -144,6 +165,85 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector { return true } + /** + * Determine if the current hashtag is muted, and update the UI state accordingly. + */ + private fun updateMuteTagMenuItems() { + val tag = hashtag ?: return + + muteTagItem?.isVisible = true + muteTagItem?.isEnabled = false + unmuteTagItem?.isVisible = false + + mastodonApi.getFilters().observeOn(AndroidSchedulers.mainThread()) + .autoDispose(AndroidLifecycleScopeProvider.from(this, Lifecycle.Event.ON_DESTROY)) + .subscribe { filters -> + for (filter in filters) { + if ((tag == filter.phrase) and filter.context.contains(Filter.HOME)) { + Log.d(TAG, "Tag $hashtag is filtered") + muteTagItem?.isVisible = false + unmuteTagItem?.isVisible = true + mutedFilter = filter + return@subscribe + } + } + + Log.d(TAG, "Tag $hashtag is not filtered") + mutedFilter = null + muteTagItem?.isEnabled = true + muteTagItem?.isVisible = true + muteTagItem?.isVisible = true + } + } + + private fun muteTag(): Boolean { + val tag = hashtag ?: return true + + lifecycleScope.launch { + mastodonApi.createFilter( + tag, + listOf(Filter.HOME), + irreversible = false, + wholeWord = true, + expiresInSeconds = null + ).fold( + { filter -> + mutedFilter = filter + muteTagItem?.isVisible = false + unmuteTagItem?.isVisible = true + eventHub.dispatch(PreferenceChangedEvent(filter.context[0])) + }, + { + Snackbar.make(binding.root, getString(R.string.error_muting_hashtag_format, tag), Snackbar.LENGTH_SHORT).show() + Log.e(TAG, "Failed to mute #$tag", it) + } + ) + } + + return true + } + + private fun unmuteTag(): Boolean { + val filter = mutedFilter ?: return true + + lifecycleScope.launch { + mastodonApi.deleteFilter(filter.id).fold( + { + muteTagItem?.isVisible = true + unmuteTagItem?.isVisible = false + eventHub.dispatch(PreferenceChangedEvent(filter.context[0])) + mutedFilter = null + }, + { + Snackbar.make(binding.root, getString(R.string.error_unmuting_hashtag_format, filter.phrase), Snackbar.LENGTH_SHORT).show() + Log.e(TAG, "Failed to unmute #${filter.phrase}", it) + } + ) + } + + return true + } + override fun androidInjector() = dispatchingAndroidInjector companion object { diff --git a/app/src/main/res/menu/view_hashtag_toolbar.xml b/app/src/main/res/menu/view_hashtag_toolbar.xml index 159593dc..fb22c7e0 100644 --- a/app/src/main/res/menu/view_hashtag_toolbar.xml +++ b/app/src/main/res/menu/view_hashtag_toolbar.xml @@ -17,5 +17,18 @@ app:iconTint="?attr/colorOnSurface" android:icon="@drawable/ic_person_remove_24dp" /> + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ac478113..1965c4db 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -25,6 +25,8 @@ Error following #%s Error unfollowing #%s This instance does not support following hashtags. + Error muting #%s + Error unmuting #%s Login Home From 24fccc3bbc57dc2bcfcff379924ad890f030c755 Mon Sep 17 00:00:00 2001 From: fruyek Date: Mon, 5 Dec 2022 14:36:51 +0100 Subject: [PATCH 017/232] Show emoji codes on long press in the picker (#2981) --- .../main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt index dc9ec70d..d3412e5b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt @@ -17,6 +17,7 @@ package com.keylesspalace.tusky.adapter import android.view.LayoutInflater import android.view.ViewGroup +import androidx.appcompat.widget.TooltipCompat import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide import com.keylesspalace.tusky.databinding.ItemEmojiButtonBinding @@ -52,6 +53,7 @@ class EmojiAdapter( } emojiImageView.contentDescription = emoji.shortcode + TooltipCompat.setTooltipText(emojiImageView, emoji.shortcode) } } From 0b921f3c2625f8bf043e9cc0bdcdf9d8fbde0cb9 Mon Sep 17 00:00:00 2001 From: Eva Tatarka Date: Mon, 5 Dec 2022 05:44:32 -0800 Subject: [PATCH 018/232] Increase the size of the accept/reject follow buttons (#2987) Also swapped the order to follow android's convention of having the positive button on the right. --- .../main/res/layout/item_follow_request.xml | 46 +++++++++---------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/app/src/main/res/layout/item_follow_request.xml b/app/src/main/res/layout/item_follow_request.xml index 8fe743d9..e31ec6cf 100644 --- a/app/src/main/res/layout/item_follow_request.xml +++ b/app/src/main/res/layout/item_follow_request.xml @@ -4,8 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:paddingLeft="16dp" - android:paddingRight="16dp" + android:paddingStart="16dp" android:paddingBottom="10dp"> @@ -62,16 +61,32 @@ android:textColor="?android:textColorSecondary" android:textSize="?attr/status_text_medium" app:layout_constraintBottom_toBottomOf="@id/avatar" - app:layout_constraintEnd_toStartOf="@id/acceptButton" + app:layout_constraintEnd_toStartOf="@id/rejectButton" app:layout_constraintStart_toEndOf="@id/avatar" app:layout_constraintTop_toBottomOf="@id/displayNameTextView" tools:text="\@username" /> + + - - - + app:srcCompat="@drawable/ic_check_24dp" /> From f3962058dc2ce08930d9d9b2a84a14138a5395ac Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Mon, 5 Dec 2022 14:44:52 +0100 Subject: [PATCH 019/232] fix blocking accounts in thread view (#2988) --- .../tusky/components/viewthread/ViewThreadFragment.kt | 6 ++++++ .../tusky/components/viewthread/ViewThreadViewModel.kt | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadFragment.kt index 9fe91b92..2f177b5f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadFragment.kt @@ -169,6 +169,12 @@ class ViewThreadFragment : SFragment(), OnRefreshListener, StatusActionListener, } } is ThreadUiState.Success -> { + if (uiState.statuses.none { viewData -> viewData.isDetailed }) { + // no detailed statuses available, e.g. because author is blocked + activity?.finish() + return@collect + } + adapter.submitList(uiState.statuses) { if (viewModel.isInitialLoad) { viewModel.isInitialLoad = false 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 b4a8a03e..f4d7e14d 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 @@ -262,7 +262,7 @@ class ViewThreadViewModel @Inject constructor( updateSuccess { uiState -> uiState.copy( statuses = uiState.statuses.filter { viewData -> - viewData.status.account.id == accountId + viewData.status.account.id != accountId } ) } From 11de43f470456b937f302798b0129bb082dae406 Mon Sep 17 00:00:00 2001 From: Levi Bard Date: Mon, 5 Dec 2022 14:49:09 +0100 Subject: [PATCH 020/232] Sort language lists by the localized language name (#2991) --- app/src/main/java/com/keylesspalace/tusky/util/LocaleUtils.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/util/LocaleUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/LocaleUtils.kt index 1cc3f3be..316e14d0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/LocaleUtils.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/LocaleUtils.kt @@ -82,7 +82,7 @@ fun getLocaleList(initialLanguage: String): List { it.country.isNullOrEmpty() && it.script.isNullOrEmpty() && it.variant.isNullOrEmpty() - } + }.sortedBy { it.displayName } ) ensureLanguageIsFirst(locales, initialLanguage) return locales From 965d51100c4f772a99950d3e519be06b4dca46f7 Mon Sep 17 00:00:00 2001 From: Nik Clayton Date: Mon, 5 Dec 2022 14:51:45 +0100 Subject: [PATCH 021/232] Cleanup NotificationFragment to make future conversion to Kotlin easier (#2993) * Convert NotificationsFragment to use view binding * Use requireContext() in places a context is required Removes a nullness warning. * Simplify code by using .sublist() and .contains() Removes a lint warning. * Add @NonNull annotations to onViewTag and onViewAccount * Use consistent comment styles --- .../tusky/fragment/NotificationsFragment.java | 180 +++++++++--------- 1 file changed, 85 insertions(+), 95 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java index 8ee0bf01..99543b80 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java @@ -30,10 +30,8 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; -import android.widget.Button; import android.widget.ListView; import android.widget.PopupWindow; -import android.widget.ProgressBar; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -65,6 +63,7 @@ import com.keylesspalace.tusky.appstore.FavoriteEvent; import com.keylesspalace.tusky.appstore.PinEvent; import com.keylesspalace.tusky.appstore.PreferenceChangedEvent; import com.keylesspalace.tusky.appstore.ReblogEvent; +import com.keylesspalace.tusky.databinding.FragmentTimelineNotificationsBinding; import com.keylesspalace.tusky.db.AccountEntity; import com.keylesspalace.tusky.db.AccountManager; import com.keylesspalace.tusky.di.Injectable; @@ -87,7 +86,6 @@ import com.keylesspalace.tusky.util.NotificationTypeConverterKt; import com.keylesspalace.tusky.util.PairedList; import com.keylesspalace.tusky.util.StatusDisplayOptions; import com.keylesspalace.tusky.util.ViewDataUtils; -import com.keylesspalace.tusky.view.BackgroundMessageView; import com.keylesspalace.tusky.view.EndlessOnScrollListener; import com.keylesspalace.tusky.viewdata.AttachmentViewData; import com.keylesspalace.tusky.viewdata.NotificationViewData; @@ -158,16 +156,11 @@ public class NotificationsFragment extends SFragment implements @Inject EventHub eventHub; - private SwipeRefreshLayout swipeRefreshLayout; - private RecyclerView recyclerView; - private ProgressBar progressBar; - private BackgroundMessageView statusView; - private AppBarLayout appBarOptions; + private FragmentTimelineNotificationsBinding binding; private LinearLayoutManager layoutManager; private EndlessOnScrollListener scrollListener; private NotificationsAdapter adapter; - private Button buttonFilter; private boolean hideFab; private boolean topLoading; private boolean bottomLoading; @@ -211,35 +204,29 @@ public class NotificationsFragment extends SFragment implements @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - View rootView = inflater.inflate(R.layout.fragment_timeline_notifications, container, false); + binding = FragmentTimelineNotificationsBinding.inflate(inflater, container, false); @NonNull Context context = inflater.getContext(); // from inflater to silence warning - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()); boolean showNotificationsFilterSetting = preferences.getBoolean("showNotificationsFilter", true); - //Clear notifications on filter visibility change to force refresh + // Clear notifications on filter visibility change to force refresh if (showNotificationsFilterSetting != showNotificationsFilter) notifications.clear(); showNotificationsFilter = showNotificationsFilterSetting; // Setup the SwipeRefreshLayout. - swipeRefreshLayout = rootView.findViewById(R.id.swipeRefreshLayout); - recyclerView = rootView.findViewById(R.id.recyclerView); - progressBar = rootView.findViewById(R.id.progressBar); - statusView = rootView.findViewById(R.id.statusView); - appBarOptions = rootView.findViewById(R.id.appBarOptions); - - swipeRefreshLayout.setOnRefreshListener(this); - swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue); + binding.swipeRefreshLayout.setOnRefreshListener(this); + binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue); loadNotificationsFilter(); // Setup the RecyclerView. - recyclerView.setHasFixedSize(true); + binding.recyclerView.setHasFixedSize(true); layoutManager = new LinearLayoutManager(context); - recyclerView.setLayoutManager(layoutManager); - recyclerView.setAccessibilityDelegateCompat( - new ListStatusAccessibilityDelegate(recyclerView, this, (pos) -> { + binding.recyclerView.setLayoutManager(layoutManager); + binding.recyclerView.setAccessibilityDelegateCompat( + new ListStatusAccessibilityDelegate(binding.recyclerView, this, (pos) -> { NotificationViewData notification = notifications.getPairedItemOrNull(pos); // We support replies only for now if (notification instanceof NotificationViewData.Concrete) { @@ -249,7 +236,7 @@ public class NotificationsFragment extends SFragment implements } })); - recyclerView.addItemDecoration(new DividerItemDecoration(context, DividerItemDecoration.VERTICAL)); + binding.recyclerView.addItemDecoration(new DividerItemDecoration(context, DividerItemDecoration.VERTICAL)); StatusDisplayOptions statusDisplayOptions = new StatusDisplayOptions( preferences.getBoolean("animateGifAvatars", false), @@ -268,7 +255,7 @@ public class NotificationsFragment extends SFragment implements dataSource, statusDisplayOptions, this, this, this); alwaysShowSensitiveMedia = accountManager.getActiveAccount().getAlwaysShowSensitiveMedia(); alwaysOpenSpoiler = accountManager.getActiveAccount().getAlwaysOpenSpoiler(); - recyclerView.setAdapter(adapter); + binding.recyclerView.setAdapter(adapter); topLoading = false; bottomLoading = false; @@ -276,43 +263,47 @@ public class NotificationsFragment extends SFragment implements updateAdapter(); - Button buttonClear = rootView.findViewById(R.id.buttonClear); - buttonClear.setOnClickListener(v -> confirmClearNotifications()); - buttonFilter = rootView.findViewById(R.id.buttonFilter); - buttonFilter.setOnClickListener(v -> showFilterMenu()); + binding.buttonClear.setOnClickListener(v -> confirmClearNotifications()); + binding.buttonFilter.setOnClickListener(v -> showFilterMenu()); if (notifications.isEmpty()) { - swipeRefreshLayout.setEnabled(false); + binding.swipeRefreshLayout.setEnabled(false); sendFetchNotificationsRequest(null, null, FetchEnd.BOTTOM, -1); } else { - progressBar.setVisibility(View.GONE); + binding.progressBar.setVisibility(View.GONE); } - ((SimpleItemAnimator) recyclerView.getItemAnimator()).setSupportsChangeAnimations(false); + ((SimpleItemAnimator) binding.recyclerView.getItemAnimator()).setSupportsChangeAnimations(false); updateFilterVisibility(); - return rootView; + return binding.getRoot(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; } private void updateFilterVisibility() { CoordinatorLayout.LayoutParams params = - (CoordinatorLayout.LayoutParams) swipeRefreshLayout.getLayoutParams(); + (CoordinatorLayout.LayoutParams) binding.swipeRefreshLayout.getLayoutParams(); if (showNotificationsFilter && !showingError) { - appBarOptions.setExpanded(true, false); - appBarOptions.setVisibility(View.VISIBLE); - //Set content behaviour to hide filter on scroll + binding.appBarOptions.setExpanded(true, false); + binding.appBarOptions.setVisibility(View.VISIBLE); + // Set content behaviour to hide filter on scroll params.setBehavior(new AppBarLayout.ScrollingViewBehavior()); } else { - appBarOptions.setExpanded(false, false); - appBarOptions.setVisibility(View.GONE); - //Clear behaviour to hide app bar + binding.appBarOptions.setExpanded(false, false); + binding.appBarOptions.setVisibility(View.GONE); + // Clear behaviour to hide app bar params.setBehavior(null); } } private void confirmClearNotifications() { - new AlertDialog.Builder(getContext()) + new AlertDialog.Builder(requireContext()) .setMessage(R.string.notification_clear_text) .setPositiveButton(android.R.string.ok, (DialogInterface dia, int which) -> clearNotifications()) .setNegativeButton(android.R.string.cancel, null) @@ -325,10 +316,10 @@ public class NotificationsFragment extends SFragment implements Activity activity = getActivity(); if (activity == null) throw new AssertionError("Activity is null"); - /* This is delayed until onActivityCreated solely because MainActivity.composeButton isn't - * guaranteed to be set until then. - * Use a modified scroll listener that both loads more notificationsEnabled as it goes, and hides - * the compose button on down-scroll. */ + // This is delayed until onActivityCreated solely because MainActivity.composeButton + // isn't guaranteed to be set until then. + // Use a modified scroll listener that both loads more notificationsEnabled as it + // goes, and hides the compose button on down-scroll. SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity); hideFab = preferences.getBoolean("fabHide", false); scrollListener = new EndlessOnScrollListener(layoutManager) { @@ -342,9 +333,9 @@ public class NotificationsFragment extends SFragment implements if (composeButton != null) { if (hideFab) { if (dy > 0 && composeButton.isShown()) { - composeButton.hide(); // hides the button if we're scrolling down + composeButton.hide(); // Hides the button if we're scrolling down } else if (dy < 0 && !composeButton.isShown()) { - composeButton.show(); // shows it if we are scrolling up + composeButton.show(); // Shows it if we are scrolling up } } else if (!composeButton.isShown()) { composeButton.show(); @@ -358,7 +349,7 @@ public class NotificationsFragment extends SFragment implements } }; - recyclerView.addOnScrollListener(scrollListener); + binding.recyclerView.addOnScrollListener(scrollListener); eventHub.getEvents() .observeOn(AndroidSchedulers.mainThread()) @@ -382,7 +373,7 @@ public class NotificationsFragment extends SFragment implements @Override public void onRefresh() { - this.statusView.setVisibility(View.GONE); + binding.statusView.setVisibility(View.GONE); this.showingError = false; Either first = CollectionsKt.firstOrNull(this.notifications); String topId; @@ -518,7 +509,7 @@ public class NotificationsFragment extends SFragment implements @Override public void onLoadMore(int position) { - //check bounds before accessing list, + // Check bounds before accessing list, if (notifications.size() >= position && position > 0) { Notification previous = notifications.get(position - 1).asRightOrNull(); Notification next = notifications.get(position + 1).asRightOrNull(); @@ -540,7 +531,6 @@ public class NotificationsFragment extends SFragment implements @Override public void onContentCollapsedChange(boolean isCollapsed, int position) { updateViewDataAt(position, (vd) -> vd.copyWithCollapsed(isCollapsed)); - ; } private void updateStatus(String statusId, Function mapper) { @@ -615,28 +605,28 @@ public class NotificationsFragment extends SFragment implements } private void clearNotifications() { - //Cancel all ongoing requests - swipeRefreshLayout.setRefreshing(false); + // Cancel all ongoing requests + binding.swipeRefreshLayout.setRefreshing(false); resetNotificationsLoad(); - //Show friend elephant - this.statusView.setVisibility(View.VISIBLE); - this.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null); + // Show friend elephant + binding.statusView.setVisibility(View.VISIBLE); + binding.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null); updateFilterVisibility(); - //Update adapter + // Update adapter updateAdapter(); - //Execute clear notifications request + // Execute clear notifications request mastodonApi.clearNotifications() .observeOn(AndroidSchedulers.mainThread()) .to(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) .subscribe( response -> { - // nothing to do + // Nothing to do }, throwable -> { - //Reload notifications on failure + // Reload notifications on failure fullyRefreshWithProgressBar(true); }); } @@ -646,10 +636,10 @@ public class NotificationsFragment extends SFragment implements bottomLoading = false; topLoading = false; - //Disable load more + // Disable load more bottomId = null; - //Clear exists notifications + // Clear exists notifications notifications.clear(); } @@ -688,7 +678,7 @@ public class NotificationsFragment extends SFragment implements window.setFocusable(true); window.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT); window.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT); - window.showAsDropDown(buttonFilter); + window.showAsDropDown(binding.buttonFilter); } @@ -756,12 +746,12 @@ public class NotificationsFragment extends SFragment implements } @Override - public void onViewTag(String tag) { + public void onViewTag(@NonNull String tag) { super.viewTag(tag); } @Override - public void onViewAccount(String id) { + public void onViewAccount(@NonNull String id) { super.viewAccount(id); } @@ -805,13 +795,13 @@ public class NotificationsFragment extends SFragment implements @Override public void onViewReport(String reportId) { - LinkHelper.openLink(getContext(), String.format("https://%s/admin/reports/%s", accountManager.getActiveAccount().getDomain(), reportId)); + LinkHelper.openLink(requireContext(), String.format("https://%s/admin/reports/%s", accountManager.getActiveAccount().getDomain(), reportId)); } private void onPreferenceChanged(String key) { switch (key) { case "fabHide": { - hideFab = PreferenceManager.getDefaultSharedPreferences(getContext()).getBoolean("fabHide", false); + hideFab = PreferenceManager.getDefaultSharedPreferences(requireContext()).getBoolean("fabHide", false); break; } case "mediaPreviewEnabled": { @@ -824,7 +814,7 @@ public class NotificationsFragment extends SFragment implements } case "showNotificationsFilter": { if (isAdded()) { - showNotificationsFilter = PreferenceManager.getDefaultSharedPreferences(getContext()).getBoolean("showNotificationsFilter", true); + showNotificationsFilter = PreferenceManager.getDefaultSharedPreferences(requireContext()).getBoolean("showNotificationsFilter", true); updateFilterVisibility(); fullyRefreshWithProgressBar(true); } @@ -840,7 +830,7 @@ public class NotificationsFragment extends SFragment implements } private void removeAllByAccountId(String accountId) { - // using iterator to safely remove items while iterating + // Using iterator to safely remove items while iterating Iterator> iterator = notifications.iterator(); while (iterator.hasNext()) { Either notification = iterator.next(); @@ -854,7 +844,7 @@ public class NotificationsFragment extends SFragment implements private void onLoadMore() { if (bottomId == null) { - // already loaded everything + // Already loaded everything return; } @@ -884,7 +874,7 @@ public class NotificationsFragment extends SFragment implements private void jumpToTop() { if (isAdded()) { - appBarOptions.setExpanded(true, false); + binding.appBarOptions.setExpanded(true, false); layoutManager.scrollToPosition(0); scrollListener.reset(); } @@ -892,8 +882,8 @@ public class NotificationsFragment extends SFragment implements private void sendFetchNotificationsRequest(String fromId, String uptoId, final FetchEnd fetchEnd, final int pos) { - /* If there is a fetch already ongoing, record however many fetches are requested and - * fulfill them after it's complete. */ + // If there is a fetch already ongoing, record however many fetches are requested and + // fulfill them after it's complete. if (fetchEnd == FetchEnd.TOP && topLoading) { return; } @@ -969,18 +959,18 @@ public class NotificationsFragment extends SFragment implements } if (notifications.size() == 0 && adapter.getItemCount() == 0) { - this.statusView.setVisibility(View.VISIBLE); - this.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null); + binding.statusView.setVisibility(View.VISIBLE); + binding.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null); } updateFilterVisibility(); - swipeRefreshLayout.setEnabled(true); - swipeRefreshLayout.setRefreshing(false); - progressBar.setVisibility(View.GONE); + binding.swipeRefreshLayout.setEnabled(true); + binding.swipeRefreshLayout.setRefreshing(false); + binding.progressBar.setVisibility(View.GONE); } private void onFetchNotificationsFailure(Throwable throwable, FetchEnd fetchEnd, int position) { - swipeRefreshLayout.setRefreshing(false); + binding.swipeRefreshLayout.setRefreshing(false); if (fetchEnd == FetchEnd.MIDDLE && !notifications.get(position).isRight()) { Placeholder placeholder = notifications.get(position).asLeft(); NotificationViewData placeholderVD = @@ -988,18 +978,18 @@ public class NotificationsFragment extends SFragment implements notifications.setPairedItem(position, placeholderVD); updateAdapter(); } else if (this.notifications.isEmpty()) { - this.statusView.setVisibility(View.VISIBLE); - swipeRefreshLayout.setEnabled(false); + binding.statusView.setVisibility(View.VISIBLE); + binding.swipeRefreshLayout.setEnabled(false); this.showingError = true; if (throwable instanceof IOException) { - this.statusView.setup(R.drawable.elephant_offline, R.string.error_network, __ -> { - this.progressBar.setVisibility(View.VISIBLE); + binding.statusView.setup(R.drawable.elephant_offline, R.string.error_network, __ -> { + binding.progressBar.setVisibility(View.VISIBLE); this.onRefresh(); return Unit.INSTANCE; }); } else { - this.statusView.setup(R.drawable.elephant_error, R.string.error_generic, __ -> { - this.progressBar.setVisibility(View.VISIBLE); + binding.statusView.setup(R.drawable.elephant_error, R.string.error_generic, __ -> { + binding.progressBar.setVisibility(View.VISIBLE); this.onRefresh(); return Unit.INSTANCE; }); @@ -1015,7 +1005,7 @@ public class NotificationsFragment extends SFragment implements bottomLoading = false; } - progressBar.setVisibility(View.GONE); + binding.progressBar.setVisibility(View.GONE); } private void saveNewestNotificationId(List notifications) { @@ -1052,8 +1042,8 @@ public class NotificationsFragment extends SFragment implements notifications.addAll(liftedNew); } else { int index = notifications.indexOf(liftedNew.get(newNotifications.size() - 1)); - for (int i = 0; i < index; i++) { - notifications.remove(0); + if (index > 0) { + notifications.subList(0, index).clear(); } int newIndex = liftedNew.indexOf(notifications.get(0)); @@ -1077,7 +1067,7 @@ public class NotificationsFragment extends SFragment implements int end = notifications.size(); List> liftedNew = liftNotificationList(newNotifications); Either last = notifications.get(end - 1); - if (last != null && liftedNew.indexOf(last) == -1) { + if (last != null && !liftedNew.contains(last)) { notifications.addAll(liftedNew); updateAdapter(); } @@ -1115,8 +1105,8 @@ public class NotificationsFragment extends SFragment implements private void fullyRefreshWithProgressBar(boolean isShow) { resetNotificationsLoad(); if (isShow) { - progressBar.setVisibility(View.VISIBLE); - statusView.setVisibility(View.GONE); + binding.progressBar.setVisibility(View.VISIBLE); + binding.statusView.setVisibility(View.GONE); } updateAdapter(); sendFetchNotificationsRequest(null, null, FetchEnd.TOP, -1); @@ -1155,7 +1145,7 @@ public class NotificationsFragment extends SFragment implements // scroll up when new items at the top are loaded while being at the start // https://github.com/tuskyapp/Tusky/pull/1905#issuecomment-677819724 if (position == 0 && context != null && adapter.getItemCount() != count) { - recyclerView.scrollBy(0, Utils.dpToPx(context, -30)); + binding.recyclerView.scrollBy(0, Utils.dpToPx(context, -30)); } } } @@ -1210,7 +1200,7 @@ public class NotificationsFragment extends SFragment implements @Override public Object getChangePayload(@NonNull NotificationViewData oldItem, @NonNull NotificationViewData newItem) { if (oldItem.deepEquals(newItem)) { - //If items are equal - update timestamp only + // If items are equal - update timestamp only return Collections.singletonList(StatusBaseViewHolder.Key.KEY_CREATED); } else // If items are different - update a whole view holder @@ -1236,7 +1226,7 @@ public class NotificationsFragment extends SFragment implements * Auto dispose observable on pause */ private void startUpdateTimestamp() { - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()); boolean useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false); if (!useAbsoluteTime) { Observable.interval(0, 1, TimeUnit.MINUTES) From 36befdebe2c24ca064e1dc78fb90a6a307ef500f Mon Sep 17 00:00:00 2001 From: Eva Tatarka Date: Mon, 5 Dec 2022 10:05:46 -0800 Subject: [PATCH 022/232] Add a touch delegate to increase action touch targets to 48dp (#2872) * Add a touch delegate to increase action touch targets to 48dp Fixes #2825 * Adjust layout to make action buttons larger * Remove 4dp vertical margin --- .../tusky/adapter/StatusBaseViewHolder.java | 4 ++ .../tusky/util/TouchDelegateHelper.kt | 51 ++++++++++++++++++ app/src/main/res/layout/item_status.xml | 51 ++++++++++-------- .../main/res/layout/item_status_detailed.xml | 52 +++++++++++++------ 4 files changed, 118 insertions(+), 40 deletions(-) create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/TouchDelegateHelper.kt 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 c4978091..e16c4d38 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java @@ -55,6 +55,7 @@ import com.keylesspalace.tusky.util.LinkHelper; import com.keylesspalace.tusky.util.StatusDisplayOptions; import com.keylesspalace.tusky.util.ThemeUtils; import com.keylesspalace.tusky.util.TimestampUtils; +import com.keylesspalace.tusky.util.TouchDelegateHelper; import com.keylesspalace.tusky.view.MediaPreviewImageView; import com.keylesspalace.tusky.view.MediaPreviewLayout; import com.keylesspalace.tusky.viewdata.PollOptionViewData; @@ -63,6 +64,7 @@ import com.keylesspalace.tusky.viewdata.PollViewDataKt; import com.keylesspalace.tusky.viewdata.StatusViewData; import java.text.NumberFormat; +import java.util.Arrays; import java.util.Date; import java.util.List; @@ -170,6 +172,8 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { this.avatarRadius24dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_24dp); mediaPreviewUnloaded = new ColorDrawable(ThemeUtils.getColor(itemView.getContext(), R.attr.colorBackgroundAccent)); + + TouchDelegateHelper.expandTouchSizeToFillRow((ViewGroup) itemView, Arrays.asList(replyButton, reblogButton, favouriteButton, bookmarkButton, moreButton)); } protected void setDisplayName(String name, List customEmojis, StatusDisplayOptions statusDisplayOptions) { diff --git a/app/src/main/java/com/keylesspalace/tusky/util/TouchDelegateHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/TouchDelegateHelper.kt new file mode 100644 index 00000000..dc382851 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/TouchDelegateHelper.kt @@ -0,0 +1,51 @@ +@file:JvmName("TouchDelegateHelper") + +package com.keylesspalace.tusky.util + +import android.graphics.Rect +import android.view.MotionEvent +import android.view.TouchDelegate +import android.view.View +import android.view.ViewGroup + +/** + * Expands the touch area of the give row of views to fill the space in-between them, using a + * [TouchDelegate]. + */ +fun ViewGroup.expandTouchSizeToFillRow(children: List) { + addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> + touchDelegate = CompositeTouchDelegate( + this, + children.mapIndexed { i, view -> + val rect = Rect() + view.getHitRect(rect) + val left = children.getOrNull(i - 1) + if (left != null) { + // extend half-way to previous view + rect.left -= (view.left - left.right) / 2 + } + val right = children.getOrNull(i + 1) + if (right != null) { + // extend half-way to next view + rect.right += (right.left - view.right) / 2 + } + TouchDelegate(rect, view) + } + ) + } +} + +private class CompositeTouchDelegate(view: View, private val delegates: List) : + TouchDelegate(Rect(), view) { + + override fun onTouchEvent(event: MotionEvent): Boolean { + var res = false + val x = event.x + val y = event.y + for (delegate in delegates) { + event.setLocation(x, y) + res = delegate.onTouchEvent(event) || res + } + return res + } +} diff --git a/app/src/main/res/layout/item_status.xml b/app/src/main/res/layout/item_status.xml index 68e2bbb5..38a2ac35 100644 --- a/app/src/main/res/layout/item_status.xml +++ b/app/src/main/res/layout/item_status.xml @@ -8,15 +8,15 @@ android:layout_height="wrap_content" android:clipChildren="false" android:clipToPadding="false" - android:focusable="true" - android:paddingLeft="14dp" - android:paddingRight="14dp"> + android:focusable="true"> - + @@ -267,6 +273,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginTop="4dp" + android:layout_marginEnd="14dp" android:layout_marginBottom="4dp" android:nestedScrollingEnabled="false" app:layout_constraintEnd_toEndOf="parent" @@ -296,6 +303,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginTop="6dp" + android:layout_marginEnd="14dp" android:textSize="?attr/status_text_medium" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="@id/status_display_name" @@ -305,13 +313,11 @@ + tools:text="1+" /> - diff --git a/app/src/main/res/layout/item_status_detailed.xml b/app/src/main/res/layout/item_status_detailed.xml index f30ec627..d165c7ca 100644 --- a/app/src/main/res/layout/item_status_detailed.xml +++ b/app/src/main/res/layout/item_status_detailed.xml @@ -7,14 +7,13 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:clipChildren="false" - android:clipToPadding="false" - android:paddingLeft="14dp" - android:paddingRight="14dp"> + android:clipToPadding="false"> @@ -98,6 +100,7 @@ style="@style/TuskyButton.Outlined" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_marginStart="14dp" android:layout_marginTop="4dp" android:layout_marginBottom="4dp" android:importantForAccessibility="no" @@ -117,14 +120,16 @@ android:id="@+id/status_content" android:layout_width="0dp" android:layout_height="wrap_content" + android:layout_marginStart="14dp" android:layout_marginTop="4dp" + android:layout_marginEnd="14dp" android:layout_marginBottom="4dp" android:focusable="true" android:hyphenationFrequency="full" android:importantForAccessibility="no" android:lineSpacingMultiplier="1.1" - android:textIsSelectable="true" android:textColor="?android:textColorPrimary" + android:textIsSelectable="true" android:textSize="?attr/status_text_large" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -135,7 +140,9 @@ android:id="@+id/status_card_view" android:layout_width="match_parent" android:layout_height="wrap_content" + android:layout_marginStart="14dp" android:layout_marginTop="8dp" + android:layout_marginEnd="14dp" android:background="@drawable/card_frame" android:clipChildren="true" android:foreground="?attr/selectableItemBackground" @@ -201,7 +208,9 @@ android:id="@+id/status_media_preview_container" android:layout_width="match_parent" android:layout_height="wrap_content" + android:layout_marginStart="14dp" android:layout_marginTop="12dp" + android:layout_marginEnd="14dp" android:layout_marginBottom="4dp" android:background="@drawable/media_preview_outline" android:importantForAccessibility="noHideDescendants" @@ -215,7 +224,9 @@ android:id="@+id/status_poll_options" android:layout_width="0dp" android:layout_height="wrap_content" + android:layout_marginStart="14dp" android:layout_marginTop="4dp" + android:layout_marginEnd="14dp" android:layout_marginBottom="4dp" android:nestedScrollingEnabled="false" app:layout_constraintEnd_toEndOf="parent" @@ -227,6 +238,7 @@ style="@style/TuskyButton.Outlined" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_marginStart="14dp" android:layout_marginTop="4dp" android:gravity="center" android:minWidth="150dp" @@ -244,7 +256,9 @@ android:id="@+id/status_poll_description" android:layout_width="0dp" android:layout_height="wrap_content" + android:layout_marginStart="14dp" android:layout_marginTop="6dp" + android:layout_marginEnd="14dp" android:textSize="?attr/status_text_medium" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -255,7 +269,9 @@ android:id="@+id/status_timestamp_info" android:layout_width="0dp" android:layout_height="wrap_content" + android:layout_marginStart="14dp" android:layout_marginTop="10dp" + android:layout_marginEnd="14dp" android:drawablePadding="4dp" android:importantForAccessibility="no" android:textColor="?android:textColorTertiary" @@ -270,7 +286,9 @@ android:layout_width="match_parent" android:layout_height="1dp" android:layout_below="@id/status_timestamp_info" + android:layout_marginStart="14dp" android:layout_marginTop="6dp" + android:layout_marginEnd="14dp" android:background="?android:attr/listDivider" android:importantForAccessibility="no" android:paddingStart="16dp" @@ -318,7 +336,9 @@ android:id="@+id/status_buttons_divider" android:layout_width="match_parent" android:layout_height="1dp" + android:layout_marginStart="14dp" android:layout_marginTop="6dp" + android:layout_marginEnd="14dp" android:background="?android:attr/listDivider" android:importantForAccessibility="no" android:paddingStart="16dp" @@ -328,10 +348,8 @@ Date: Mon, 5 Dec 2022 19:10:55 +0100 Subject: [PATCH 023/232] Upgrade gradle to 7.6 and AGP to 7.3.1 (#2977) --- gradle/libs.versions.toml | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3f384f4e..57adc585 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "7.2.2" +agp = "7.3.1" androidx-activity = "1.6.0" androidx-appcompat = "1.6.0-rc01" androidx-browser = "1.4.0" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8fad3f5a..f42e62f3 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 564caf4e9dfb6ee6659cd4ae9536f12e03df62d9 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Mon, 5 Dec 2022 19:13:15 +0100 Subject: [PATCH 024/232] don't include dependency info in apk (#2995) --- app/build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/build.gradle b/app/build.gradle index 2bf6000e..c3936ce8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -90,6 +90,10 @@ android { kotlinOptions { freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn" } + dependenciesInfo { + includeInApk false + includeInBundle false + } } // library versions are in PROJECT_ROOT/gradle/libs.versions.toml From bdeb88c41f7b0a03df0a980ee4150efb63f0a8f7 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Mon, 5 Dec 2022 19:15:28 +0100 Subject: [PATCH 025/232] respect "animate emojis" setting in emoji picker (#2996) --- .../keylesspalace/tusky/adapter/EmojiAdapter.kt | 16 ++++++++++++---- .../announcements/AnnouncementsActivity.kt | 2 +- .../tusky/components/compose/ComposeActivity.kt | 10 ++++++---- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt index d3412e5b..51aa43f7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt @@ -27,7 +27,8 @@ import java.util.Locale class EmojiAdapter( emojiList: List, - private val onEmojiSelectedListener: OnEmojiSelectedListener + private val onEmojiSelectedListener: OnEmojiSelectedListener, + private val animate: Boolean ) : RecyclerView.Adapter>() { private val emojiList: List = emojiList.filter { emoji -> emoji.visibleInPicker == null || emoji.visibleInPicker } @@ -44,9 +45,16 @@ class EmojiAdapter( val emoji = emojiList[position] val emojiImageView = holder.binding.root - Glide.with(emojiImageView) - .load(emoji.url) - .into(emojiImageView) + if (animate) { + Glide.with(emojiImageView) + .load(emoji.url) + .into(emojiImageView) + } else { + Glide.with(emojiImageView) + .asBitmap() + .load(emoji.url) + .into(emojiImageView) + } emojiImageView.setOnClickListener { onEmojiSelectedListener.onEmojiSelected(emoji.shortcode) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt index b0c6653b..b353fe86 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt @@ -122,7 +122,7 @@ class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener, } viewModel.emojis.observe(this) { - picker.adapter = EmojiAdapter(it, this) + picker.adapter = EmojiAdapter(it, this, animateEmojis) } viewModel.load() diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt index 8f85f343..83531dc4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt @@ -138,6 +138,8 @@ class ComposeActivity : private var finishingUploadDialog: ProgressDialog? = null private var photoUploadUri: Uri? = null + private val preferences by lazy { PreferenceManager.getDefaultSharedPreferences(this) } + @VisibleForTesting var maximumTootCharacters = InstanceInfoRepository.DEFAULT_CHARACTER_LIMIT var charactersReservedPerUrl = InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL @@ -205,7 +207,6 @@ class ComposeActivity : accountManager.setActiveAccount(accountId) } - val preferences = PreferenceManager.getDefaultSharedPreferences(this) val theme = preferences.getString("appTheme", ThemeUtils.APP_THEME_DEFAULT) if (theme == "black") { setTheme(R.style.TuskyDialogActivityBlackTheme) @@ -216,7 +217,7 @@ class ComposeActivity : // do not do anything when not logged in, activity will be finished in super.onCreate() anyway val activeAccount = accountManager.activeAccount ?: return - setupAvatar(preferences, activeAccount) + setupAvatar(activeAccount) val mediaAdapter = MediaPreviewAdapter( this, onAddCaption = { item -> @@ -562,7 +563,7 @@ class ComposeActivity : } } - private fun setupAvatar(preferences: SharedPreferences, activeAccount: AccountEntity) { + private fun setupAvatar(activeAccount: AccountEntity) { val actionBarSizeAttr = intArrayOf(R.attr.actionBarSize) val a = obtainStyledAttributes(null, actionBarSizeAttr) val avatarSize = a.getDimensionPixelSize(0, 1) @@ -1148,7 +1149,8 @@ class ComposeActivity : private fun setEmojiList(emojiList: List?) { if (emojiList != null) { - binding.emojiView.adapter = EmojiAdapter(emojiList, this@ComposeActivity) + val animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) + binding.emojiView.adapter = EmojiAdapter(emojiList, this@ComposeActivity, animateEmojis) enableButton(binding.composeEmojiButton, true, emojiList.isNotEmpty()) } } From 615c7adc8691ece87702c04d8825e97cdb7050e9 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Mon, 5 Dec 2022 19:15:58 +0100 Subject: [PATCH 026/232] hide "take photo" button when no Photo app is installed (#2997) --- .../keylesspalace/tusky/components/compose/ComposeActivity.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt index 83531dc4..3a827e9b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt @@ -30,6 +30,7 @@ import android.net.Uri import android.os.Build import android.os.Bundle import android.os.Parcelable +import android.provider.MediaStore import android.util.Log import android.view.KeyEvent import android.view.MenuItem @@ -511,6 +512,8 @@ class ComposeActivity : val pollIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_poll).apply { colorInt = textColor; sizeDp = 18 } binding.addPollTextActionTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(pollIcon, null, null, null) + binding.actionPhotoTake.visible(Intent(MediaStore.ACTION_IMAGE_CAPTURE).resolveActivity(packageManager) != null) + binding.actionPhotoTake.setOnClickListener { initiateCameraApp() } binding.actionPhotoPick.setOnClickListener { onMediaPick() } binding.addPollTextActionTextView.setOnClickListener { openPollDialog() } From ccaaed71e35dc06bb2dddd48989eba473efc9c31 Mon Sep 17 00:00:00 2001 From: Danial Behzadi Date: Sat, 3 Dec 2022 23:35:54 +0000 Subject: [PATCH 027/232] Translated using Weblate (Persian) Currently translated at 84.2% (16 of 19 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/fa/ --- fastlane/metadata/android/fa/changelogs/97.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fastlane/metadata/android/fa/changelogs/97.txt b/fastlane/metadata/android/fa/changelogs/97.txt index 7b810a1f..2345c1d7 100644 --- a/fastlane/metadata/android/fa/changelogs/97.txt +++ b/fastlane/metadata/android/fa/changelogs/97.txt @@ -1,9 +1,9 @@ تاسکی ۲۰٫۰ - نقشک‌کارهٔ جدید به دست https://dzuk.zone -- اکنون می‌توانید برچسب‌ها را دنبال کنید. روی برچسبی ز ده و سپس نقشک داخل نوارابزار را بزنید. +- اکنون می‌توانید برچسب‌ها را دنبال کنید. روی برچسبی زده و سپس نقشک داخل نوارابزار را بزنید. - پشتیبانی از اندروید ۱۳ - پایین‌افتادنی جدید در نمای نوشتن برای تنظیم زبان فرسته -- زبانهٔ رسانه در نمایه اکنون به رسانه‌های خسّاس احترام گذاشته و نرم‌تر بار می‌شود. +- زبانهٔ رسانه در نمایه اکنون به رسانه‌های حسّاس احترام گذاشته و نرم‌تر بار می‌شود. - اکنون می‌توان پیش از فرستادن تصویر، نقطهٔ تمرکز را تنظیم کرد - گزینهٔ جدید برای نمایش نام کاربری کاملتان در نوارابزار From 618feccd3d72c807c88e1c033469796b628df57f Mon Sep 17 00:00:00 2001 From: Luna Date: Sat, 3 Dec 2022 23:35:54 +0000 Subject: [PATCH 028/232] Translated using Weblate (Polish) Currently translated at 100.0% (19 of 19 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/pl/ --- fastlane/metadata/android/pl/changelogs/97.txt | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 fastlane/metadata/android/pl/changelogs/97.txt diff --git a/fastlane/metadata/android/pl/changelogs/97.txt b/fastlane/metadata/android/pl/changelogs/97.txt new file mode 100644 index 00000000..666a2f6c --- /dev/null +++ b/fastlane/metadata/android/pl/changelogs/97.txt @@ -0,0 +1,8 @@ +Tusky 20.0 +- Nowa ikona aplikacji autorstwa Dzuka https://dzuk.zone/ +- Można teraz obserwować hashtagi. Kliknij na hashtag, a następnie na ikonę na pasku narzędzi. +- Wsparcie Androida 13 +- Nowe menu w widoku kompozycji, umożliwiające ustawienie języka wpisu +- Zakładka z multimediami w profilach szanuje teraz wrażliwe media i ładuje się płynniej. +- Możliwe jest teraz ustawienie punktu centralnego obrazu przed opublikowaniem wpisu +- Nowa opcja pokazywania pełnej nazwy użytkownika na pasku narzędzi From 2accfd071262182a560f35c6b438e8640e5a76df Mon Sep 17 00:00:00 2001 From: UlrichKu Date: Tue, 6 Dec 2022 19:28:44 +0100 Subject: [PATCH 029/232] 2890 show warning on missing description (#2919) * issue 2890: Show a warning icon if media description is missing * issue 2890: Remove disturbing additional signs * issue 2890: Add another icon; use a snackbar; change wording; use orange as color * issue 2890: Remove now unneeded new resource * issue 2890: Use a toast (also) to avoid elevation problems * issue 2890: Use snackbar with elevation again; refactor a bit --- .../components/compose/ComposeActivity.kt | 33 +++++++++++++------ .../drawable/ic_missing_description_24dp.xml | 5 +++ app/src/main/res/layout/activity_compose.xml | 16 ++++++++- app/src/main/res/values-de/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 5 files changed, 45 insertions(+), 11 deletions(-) create mode 100644 app/src/main/res/drawable/ic_missing_description_24dp.xml diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt index 3a827e9b..c8b138b0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt @@ -188,7 +188,7 @@ class ComposeActivity : Log.w("ComposeActivity", "Edit image cancelled by user") } else { Log.w("ComposeActivity", "Edit image failed: " + result.error) - displayTransientError(R.string.error_image_edit_failed) + displayTransientMessage(R.string.error_image_edit_failed) } viewModel.cropImageItemOld = null } @@ -470,9 +470,9 @@ class ComposeActivity : lifecycleScope.launch { viewModel.uploadError.collect { throwable -> if (throwable is UploadServerError) { - displayTransientError(throwable.errorMessage) + displayTransientMessage(throwable.errorMessage) } else { - displayTransientError(R.string.error_media_upload_sending) + displayTransientMessage(R.string.error_media_upload_sending) } } } @@ -500,6 +500,9 @@ class ComposeActivity : binding.composeScheduleView.setListener(this) binding.atButton.setOnClickListener { atButtonClicked() } binding.hashButton.setOnClickListener { hashButtonClicked() } + binding.descriptionMissingWarningButton.setOnClickListener { + displayTransientMessage(R.string.hint_media_description_missing) + } val textColor = ThemeUtils.getColor(this, android.R.attr.textColorTertiary) @@ -656,15 +659,15 @@ class ComposeActivity : super.onSaveInstanceState(outState) } - private fun displayTransientError(errorMessage: String) { - val bar = Snackbar.make(binding.activityCompose, errorMessage, Snackbar.LENGTH_LONG) + private fun displayTransientMessage(message: String) { + val bar = Snackbar.make(binding.activityCompose, message, Snackbar.LENGTH_LONG) // necessary so snackbar is shown over everything bar.view.elevation = resources.getDimension(R.dimen.compose_activity_snackbar_elevation) bar.setAnchorView(R.id.composeBottomBar) bar.show() } - private fun displayTransientError(@StringRes stringId: Int) { - displayTransientError(getString(stringId)) + private fun displayTransientMessage(@StringRes stringId: Int) { + displayTransientMessage(getString(stringId)) } private fun toggleHideMedia() { @@ -674,6 +677,7 @@ class ComposeActivity : private fun updateSensitiveMediaToggle(markMediaSensitive: Boolean, contentWarningShown: Boolean) { if (viewModel.media.value.isEmpty()) { binding.composeHideMediaButton.hide() + binding.descriptionMissingWarningButton.hide() } else { binding.composeHideMediaButton.show() @ColorInt val color = if (contentWarningShown) { @@ -691,6 +695,15 @@ class ComposeActivity : } } binding.composeHideMediaButton.drawable.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN) + + var oneMediaWithoutDescription = false + for (media in viewModel.media.value) { + if (media.description == null || media.description.isEmpty()) { + oneMediaWithoutDescription = true + break + } + } + binding.descriptionMissingWarningButton.visibility = if (oneMediaWithoutDescription) View.VISIBLE else View.GONE } } @@ -760,7 +773,7 @@ class ComposeActivity : binding.emojiView.adapter?.let { if (it.itemCount == 0) { val errorMessage = getString(R.string.error_no_custom_emojis, accountManager.activeAccount!!.domain) - Toast.makeText(this, errorMessage, Toast.LENGTH_SHORT).show() + displayTransientMessage(errorMessage) } else { if (emojiBehavior.state == BottomSheetBehavior.STATE_HIDDEN || emojiBehavior.state == BottomSheetBehavior.STATE_COLLAPSED) { emojiBehavior.state = BottomSheetBehavior.STATE_EXPANDED @@ -980,7 +993,7 @@ class ComposeActivity : val photoFile: File = try { createNewImageFile(this) } catch (ex: IOException) { - displayTransientError(R.string.error_media_upload_opening) + displayTransientMessage(R.string.error_media_upload_opening) return } @@ -1050,7 +1063,7 @@ class ComposeActivity : is VideoOrImageException -> getString(R.string.error_media_upload_image_or_video) else -> getString(R.string.error_media_upload_opening) } - displayTransientError(errorString) + displayTransientMessage(errorString) } } } diff --git a/app/src/main/res/drawable/ic_missing_description_24dp.xml b/app/src/main/res/drawable/ic_missing_description_24dp.xml new file mode 100644 index 00000000..19d78d19 --- /dev/null +++ b/app/src/main/res/drawable/ic_missing_description_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/activity_compose.xml b/app/src/main/res/layout/activity_compose.xml index 902bae40..367d88ba 100644 --- a/app/src/main/res/layout/activity_compose.xml +++ b/app/src/main/res/layout/activity_compose.xml @@ -25,7 +25,7 @@ + + Anzeigename Über mich Suchen … + Medien sollten Beschreibungen haben. Keine Ergebnisse Antworten … Profilbild diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1965c4db..4bf7a371 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -195,6 +195,7 @@ Display name Bio Search… + Media should have a description. No results From 9e9aaee4e47931ec2ef95322054ad54f716d0175 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Quent=C3=AD?= Date: Tue, 6 Dec 2022 18:27:40 +0000 Subject: [PATCH 030/232] Translated using Weblate (Occitan) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 99.8% (526 of 527 strings) Translated using Weblate (Occitan) Currently translated at 98.2% (516 of 525 strings) Translated using Weblate (Occitan) Currently translated at 97.1% (510 of 525 strings) Translated using Weblate (Occitan) Currently translated at 96.7% (499 of 516 strings) Translated using Weblate (Occitan) Currently translated at 94.3% (486 of 515 strings) Translated using Weblate (Occitan) Currently translated at 94.1% (485 of 515 strings) Translated using Weblate (Occitan) Currently translated at 93.3% (477 of 511 strings) Translated using Weblate (Occitan) Currently translated at 91.3% (463 of 507 strings) Co-authored-by: Quentí Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/oc/ Translation: Tusky/Tusky --- app/src/main/res/values-oc/strings.xml | 128 ++++++++++++++++++++----- 1 file changed, 105 insertions(+), 23 deletions(-) diff --git a/app/src/main/res/values-oc/strings.xml b/app/src/main/res/values-oc/strings.xml index d093cbc2..cd94aa80 100644 --- a/app/src/main/res/values-oc/strings.xml +++ b/app/src/main/res/values-oc/strings.xml @@ -8,18 +8,18 @@ S\'es produch una error d\'autoritzacion pas identificada. L\'autoritzacion es estada regetada. Fracàs de l’obtencion del testimoni d\'iniciacion de session. - L\'estatut es tròp long ! + La publicacion es tròp longa ! Aqueste tip de fichièr se pòt pas mandar. Aqueste tip de fichièr se pòt pas dobrir. Cal permís de lectura del mèdia. Cal pemís d\'escritura dins lo mèdia. - Se pòt pas ajustar imatges e una vidèo dins lo meteis estatut. - Fracàs en mandar. - Error en enviar lo tut. + Se pòt pas ajustar imatges e una vidèo dins la meteissa publicacion. + Fracàs en mandant. + Error en enviant la publicacion. Acuèlh Notificacions Federacion - Tut + Fil Pòstes Amb responsas Abonament @@ -40,8 +40,8 @@ Desplegar Replegar I a pas res aquí. Davalatz per actualizar ! - %s a partajat vòstre tut - %s a aimat vòstre tut + %s a partejat vòstra publicacion + %s a aimat vòstra publicacion %s vos sèc Denonciar @%s Cap comentari addicional ? @@ -60,7 +60,7 @@ Quitar de blocar Rescondre los partatges Mostrar los retuts - Denonciar + Senhalar Escafar TUT TUT ! @@ -90,7 +90,7 @@ Regetar Cercar Borrolhons - Visibilitat del tut + Visibilitat de la publicacion Avis de contengut Clavièr Emoji Telecargament %1$s @@ -142,7 +142,7 @@ òm me sèc òm parteja mas publicacions òm met mos tuts en favorit - Aparença + Aparéncia Tèma de l’app Escur Luminós @@ -176,7 +176,7 @@ Seguidors nòus Notificacions de nòus seguidors Retuts - Notificacions de compartiment de vòstres tuts + Notificacions de compartiment de vòstras publicacions Preferits Notificacions s’agradan vòstres tuts %s vos mencionan @@ -189,9 +189,7 @@ Compte blocat A prepaus - Tusky es programa gratuït, liure e de còdi dobèrt. - Es publicat jols tèrms de la licéncia publica generala GNU version 3. - Podeu trobar les llicència aquí: https://www.gnu.org/licenses/gpl-3.0.ca.html + Tusky es programa gratuit, liure e de còdi dobèrt. Es publicat jols tèrmes de la licéncia publica generala GNU version 3. Podètz trobar les licéncia aquí : https://www.gnu.org/licenses/gpl-3.0.ca.html w %d lata w %d dni @@ -219,7 +219,7 @@ %d godz. temu %d min. temu %ds temu - Śledzi Cię + Obserwuje Cię Zawsze wyświetlaj wrażliwą zawartość Zawartość multimedialna Odpowiadasz na wpis autorstwa @%s @@ -233,8 +233,8 @@ Ustaw podpis Usuń Zablokuj konto - Wymaga od Ciebie ręcznej akceptacji próśb o śledzenie - Czy chcesz zapisać szkic? + Wymaga od Ciebie ręcznej akceptacji próśb o obserwacje + Czy chcesz zapisać jako szkic\? Wysyłanie wpisu… Wystąpił błąd podczas wysyłania wpisu Wysyłanie wpisów @@ -300,7 +300,7 @@ Domena %s nie jest już schowana Usunąć ten wpis\? Usunąć i napisać ponownie ten wpis\? - Czy jesteś pewien/pewna że chcesz zablokować wszystko z domeny %s\? Nie będziesz widzieć zawartości z tej domeny w żadnej osi czasu ani w twoich powiadomieniach. Twoi obserwujący z tej domeny nie będą usunięci. + Czy jesteś pewien/pewna że chcesz zablokować wszystko z domeny %s\? Nie będziesz widzieć zawartości z tej domeny w żadnej osi czasu ani w twoich powiadomieniach. Twoi obserwujący z tej domeny będą usunięci. Schowaj całą domenę głosowania zostały zakończone Osi czasu @@ -332,11 +332,17 @@ Zmień nazwę listy Usuń listę Edytuj listę - Szukaj osób, które śledzisz + Szukaj osób, których obserwujesz Dodaj konto do listy Usuń konto z listy - Wprowadź opis dla niewidomych i niedowidzących + Wprowadź opis dla niewidomych i niedowidzących +\n(maksymalna długość: %d) + Wprowadź opis dla niewidomych i niedowidzących +\n(maksymalna długość: %d) + Wprowadź opis dla niewidomych i niedowidzących +\n(maksymalna długość: %d) + Wprowadź opis dla niewidomych i niedowidzących \n(maksymalna długość: %d) Aktualny zestaw emoji Google @@ -376,7 +382,7 @@ Polubiony Publiczny Niewidoczne - Śledzący + Obserwujący Bezpośrednio Głosowanie z opcjami: %1$s, %2$s, %3$s, %4$s; %5$s Nazwa listy @@ -465,7 +471,7 @@ Nie masz żadnych szkiców. Nie masz żadnych zaplanowanych wpisów. Mastodon umożliwia wysłanie minimalnie 5 minut od zaplanowania. - Prośby o możliwość śledzenia + Prośby o obserwację Pytaj o potwierdzenie przed podbiciem Dodaj hashtag Wyciszyć @%s\? @@ -507,17 +513,17 @@ \n - liczba polubień/podbić wpisu \n - statystyki obserwujących/postów na profilach \n -\nNie będzie to miało wpływu na powiadomienia typu push, ale możesz zmienić ustawienia powiadomień ręcznie. +\nNie będzie to miało wpływu na powiadomienia, ale możesz zmienić ustawienia powiadomień ręcznie. Włącz gest przesuwania by przełączać między zakładkami Załączniki Powiadomienia o prośbach o obserwowanie - ktoś zasubskrybowany opublikował nowy wpis + ktoś, kogo zasubskrybowałem/am opublikował nowy wpis Wysłano prośbę o obserwowanie Ogłoszenia Zdrowie Anuluj subskrypcję Zasubskrybuj - Mimo tego, że twoje konto nie jest zablokowane, administracja %1$s uznała, że możesz chcieć ręcznie przejrzeć te prośby o możliwość śledzenia od tych kont. + Mimo tego, że twoje konto nie jest zablokowane, administracja %1$s uznała, że możesz chcieć ręcznie przejrzeć te prośby o obserwację od tych kont. Wpis dla którego naszkicowałeś/aś odpowiedź został usunięty Usunięto szkic Ukryj ilościowe statystyki na profilach @@ -534,7 +540,7 @@ Wyłącz wyciszenie powiadomień od %s Usuń konwersację %s opublikował/a post - %s poprosił(a) o możliwość śledzenia Cię + %s poprosił/a o możliwość obserwowania Cię Usuń z zakładek Pytaj o potwierdzenie przed dodaniem do ulubionych 14 dni @@ -545,13 +551,13 @@ 365 dni Utwórz wpis Login - %s zarejestrował(a) się + %s zarejestrował/a się Rejestracje Powiadomienia o nowych użytkownikach Powiadomienia o edycji wpisów z którymi dokonałeś/aś interakcji ktoś zarejestrował się wpis, z którym dokonałem/am interakcji został edytowany - %s edytował(a) swój wpis + %s edytował/a swój wpis Edycje wpisów Zapisywanie szkicu… Nie można załadować strony logowania. @@ -572,5 +578,26 @@ %s (🔗 %s) (bez zmian) Wystąpił błąd podczas obserwowania #%s - Wystąpił błąd podczas usuwania obserwacji #%s + Wystąpił błąd podczas odobserwowywania #%s + Naciśnij lub przeciągnij kółko, aby wybrać punkt centralny, który będzie zawsze widoczny w miniaturkach. + Dodaj lub usuń z listy + Nie udało się dodać konta do listy + Nie udało się usunąć konta z listy + Odobserwuj #%s\? + Domyślny język wpisów + Nigdy + Zawsze + Gdy wiele kont jest zalogowanych + teraz + Nie udało się ustawić punktu centralnego + dodaj reakcję + %s zgłosił/a %s + Odobserwowano #%s + jest nowe zgłoszenie + Zgłoszenia + Powiadomienia o zgłoszeniach moderacyjnych + Wybierz punkt centralny + Czy chcesz zapisać jako szkic\? (Załączniki zostaną załadowane ponownie po przywróceniu szkicu.) + Ta instancja nie wspiera obserwowania hashtagów. + Obserwowane hashtagi \ No newline at end of file From 3c8224f694687495fca060151fc644621243367d Mon Sep 17 00:00:00 2001 From: XoseM Date: Tue, 6 Dec 2022 18:27:41 +0000 Subject: [PATCH 039/232] Translated using Weblate (Galician) Currently translated at 100.0% (527 of 527 strings) Co-authored-by: XoseM Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/gl/ Translation: Tusky/Tusky --- app/src/main/res/values-gl/strings.xml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 64deaaf6..807265d5 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -559,4 +559,24 @@ Sempre Cando hai máis dunha conta activa Nunca + Notificacións sobre temas de moderación + Spam + Engadir ou quitar da lista + Fallou a adición da conta á lista + Fallou a eliminación da conta da lista + Non seguir #%s\? + Idioma de publicación por defecto + Denuncias + Non tes listas. + %s (%s) + Faltar a unha norma + Outra + Esta instancia non ten soporte para seguimento de cancelos. + Cancelos seguidos + agora + Nova denuncia en %s + %s denunciou a %s + %s · %d publicacións fixadas + Xa non segues #%s + hai unha nova denuncia \ No newline at end of file From d243f5c2fbc49249a62572b10706576f82c0087d Mon Sep 17 00:00:00 2001 From: TAKAHASHI Shuuji Date: Tue, 6 Dec 2022 18:27:41 +0000 Subject: [PATCH 040/232] Translated using Weblate (Japanese) Currently translated at 95.0% (503 of 529 strings) Co-authored-by: TAKAHASHI Shuuji Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/ja/ Translation: Tusky/Tusky --- app/src/main/res/values-ja/strings.xml | 158 +++++++++++++++++++++---- 1 file changed, 132 insertions(+), 26 deletions(-) diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index e1efb208..9dd271b9 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -15,7 +15,7 @@ メディアの書き込み許可が必要です。 画像と動画を同時に投稿することはできません。 アップロードに失敗しました。 - トゥート送信時のエラーです。 + 投稿送信時のエラーです。 ホーム 通知 ローカル @@ -44,9 +44,9 @@ 続きを読む 閉じる 何もありません。 - 現在トゥートはありません。更新するにはプルダウンしてください! - %sさんがトゥートをブーストしました - %sさんがトゥートをお気に入りに登録しました + 現在投稿はありません。更新するにはプルダウンしてください! + %sさんが投稿をブーストしました + %sさんが投稿をお気に入りに登録しました %sさんにフォローされました \@%sさんを通報 コメント @@ -55,7 +55,7 @@ ブースト お気に入り その他 - 新規投稿 + 投稿する Mastodonでログイン ログアウト アカウント %1$s からログアウトしてもよろしいですか? @@ -97,7 +97,7 @@ 拒否 検索 下書き - トゥートの公開範囲 + 投稿の公開範囲 注意書き 絵文字キーボード タブの追加 @@ -109,8 +109,8 @@ リンクをコピー %s として開く 共有先… - トゥートのURLを共有… - トゥートを共有… + 投稿URLを共有… + 投稿を共有… メディアを共有… 送信しました! ブロックを解除しました @@ -186,7 +186,7 @@ 公開 未収載 フォロワーに限定 - トゥートのテキストサイズ + 投稿のテキストサイズ 最小 @@ -197,9 +197,9 @@ 新しいフォロワー 新しいフォロワーの通知 ブースト - あなたのトゥートがブーストされたときの通知 + あなたの投稿がブーストされたときの通知 お気に入り - あなたのトゥートがお気に入りに登録されたときの通知 + あなたの投稿がお気に入りに登録されたときの通知 %sさんが返信しました %1$sさん、%2$sさん、%3$sさんと他%4$d人 %1$sさん、%2$sさん、%3$sさん @@ -226,8 +226,8 @@ https://github.com/tuskyapp/Tusky/issues Tusky公式アカウント - トゥートの内容を共有 - トゥートへのリンクを共有 + 投稿の内容を共有 + 投稿へのリンクを共有 画像 動画 フォローリクエスト中 @@ -266,19 +266,19 @@ アカウントをロック フォロワーを手動で承認する必要があります 下書きを保存しますか? - トゥートを送信中です… - トゥート送信エラー - トゥートの送信 + 投稿を送信中です… + 投稿の送信エラー + 投稿の送信中 送信がキャンセルされました - トゥートのコピーが下書きに保存されました - 新規投稿 + 投稿のコピーが下書きに保存されました + 投稿する インスタンス %s にはカスタム絵文字がありません 絵文字スタイル システムのデフォルト 最初にこれらの絵文字セットをダウンロードする必要があります 検索中… 全て開く/閉じる - トゥートを開く + 投稿を開く アプリの再起動が必要です これらの変更を適用するには、Tuskyの再起動が必要になります 後で @@ -359,7 +359,7 @@ 投票 返信 返信 - このトゥートを削除し、下書きに戻しますか? + この投稿を削除し、下書きに戻しますか? 削除 更新 リストを作成できませんでした @@ -397,9 +397,9 @@ ブックマーク 編集 ブックマーク - 予約トゥート - 予約トゥート - 予約トゥート + 予約投稿 + 予約投稿 + 予約投稿 フォロワー %1$sさん、%2$sさん フォローリクエスト @@ -416,7 +416,7 @@ 戻る 続ける 投稿 - 新規トゥート + 投稿する リスト ハッシュタグ ハッシュタグを追加 @@ -435,7 +435,7 @@ 通知を非表示 \@%sさんをミュートしますか? \@%sさんをブロックしますか? - 閲覧注意としてマークされたトゥートを常に展開する + 閲覧注意としてマークされた投稿を常に展開する メディア #%d を開く お気に入りを表示 ブーストしたユーザーを開く @@ -458,10 +458,116 @@ %s人 Mastodonにおける予約までの最小間隔は5分です。 - %sさんがトゥートしました + %sさんが投稿しました お知らせ 本当に %s のすべてをブロックするのですか? そのドメインからのコンテンツは、公開タイムラインや通知に表示されなくなります。また、そのドメインのフォロワーは削除されます。 音声 ドメイン全体を非表示 Tuskyによって提供されています + この予約投稿を削除しますか? + ログインにより %s のルールに同意することになります。 + ブックマークを削除 + 会話を削除 + 詳細情報 + %sさんがサインアップしました + リストへの追加または削除 + 投稿言語 + お気に入りに追加しました + ブックマークに追加しました + 期間 + 無期限 + 14日間 + 30日間 + 60日間 + まだリストがありません。 + ツールバーにユーザー名を表示する + %s (%s) + 下書きを保存しますか? (添付ファイルは下書きの復元時に再アップロードされます。) + 90日間 + 180日間 + 365日間 + タイムライン通知を制限する + 投稿上の数値的な統計情報を隠す + ピン留めに失敗しました + ピン留めの解除に失敗しました + リブログしました + 下書きの保存中… + デフォルトの投稿言語 + 常に表示 + 複数アカウントでのログイン時 + 表示しない + 新しい投稿 + あなたが購読した誰かが新しい投稿をしたときの通知 + 単語全体 + 画像 %s に対する操作 + (変更なし) + %s (%s) + ビデオと音声ファイルのサイズは %s MB を超えることはできません。 + 画像が編集できませんでした。 + ログイン + %s (🔗 %s) + 画像の編集 + %s のルール + このインスタンスはハッシュタグのフォローに対応していません。 + フォローしたハッシュタグ + %s のミュートが解除されました + プッシュ通知の購読許可を Tusky に与えるための現在のアカウントへの再ログインが完了しました。しかし、まだ他のアカウントはマイグレーションができていません。UnifiedPush 通知w有効にするには、他のアカウントにも切り替えて、それぞれ再ログインをしてください。 + #%s をフォロー解除しますか? + 新規ユーザーに関する通知 + 投稿の編集 + %s に関する新しい報告 + %sさんが投稿を編集しました + %s を編集しました + %sさんが %s を報告しました + キャンセル + #%s をフォロー解除しました + カスタム絵文字のアニメーションを有効化 + この会話を削除しますか? + 新しい報告があります + 購読している誰かが新しい投稿を公開しました + 誰かがサインアップしました + サインアップ + 報告 + モデレーション報告に関する通知 + 添付ファイル + 1+ + アカウントのリストへの追加に失敗しました + アカウントのリストからの削除に失敗しました + アナウンスはありません。 + 編集しました + お気に入り前に確認ダイアログを表示する + トップバーのタイトルを隠す + ウェルビーイング + このアカウントに関するプライベートメモ + 保存しました! + プロフィール上の数値的な統計情報を隠す + + %1$d 個以上のメディア添付ファイルはアップロードできません。 + + リスト %s を本当に削除しますか? + この投稿の送信に失敗しました! + 返信情報の読み込みに失敗しました + 下書きを削除しました + 下書きの返信先の投稿が削除されました + 購読する + あなたのメンタルヘルスに影響を与える可能性のある情報は隠されます。例: +\n +\n - お気に入り/ブースト/フォローの通知 +\n - 投稿のお気に入り/ブーストの数 +\n - プロフィールのフォロワー/投稿の統計情報 +\n +\n プッシュ通知には影響ありませんが、通知設定は手動で確認できます。 + 通知の確認 + 購読の解除 + プッシュ通知のサポートを有効にするにはすべてのアカウントで再ログインしてください。 + ルール違反 + スパム + その他 + 投稿する + UnifiedPush 経由でプッシュ通知を使用するには、Tusky に Mastodon サーバー上での購読許可が必要です。そのため、Tusky に与えられた OAuth スコープを変更するための再ログインが必要になります。ここかアカウント設定で再ログインのオプションを選んでも、ローカルの下書きとキャッシュは保存されます。 + #%s のフォローエラー + プッシュ通知受け取るには再ログインしてください + #%s のフォロー解除エラー + ログインページが読み込めませんでした。 + アカウントの詳細情報の読み込みに失敗しました \ No newline at end of file From 102a288abd9af5b4b069de70bf38aeb58859fcac Mon Sep 17 00:00:00 2001 From: batuhanakkurt Date: Tue, 6 Dec 2022 18:27:41 +0000 Subject: [PATCH 041/232] Translated using Weblate (Turkish) Currently translated at 78.2% (414 of 529 strings) Co-authored-by: batuhanakkurt Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/tr/ Translation: Tusky/Tusky --- app/src/main/res/values-tr/strings.xml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 36076962..edd1d4db 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -9,14 +9,14 @@ Tanımlanamayan bir yetkilendirme hatası oluştu. Yetkilendirme reddedildi. Giriş belirteci alınırken hata oluştu. - Durum çok uzun! + Post çok uzun! Bu tür bir dosya yüklenemez. Dosya açılamadı. Medya okuma izni gerekli. Medya kaydetme izni gerekli. Görüntüler ve videolar aynı duruma eklenemez. Yükleme başarısız oldu. - Toot gönderilirken hata oluştu. + Post gönderirken hata. Ana sayfa Bildirimler Yerel @@ -473,4 +473,10 @@ Üst araç çubuğunun başlığını gizle Konuşmayı sil Duyurular + Video ve ses dosyaları %s boyutunu aşamaz + Görüntü düzenlemedi + #%s takip edilirken hata + Giriş + Hesap detayları alınırken hata + Giriş ekranı yüklenemedi \ No newline at end of file From 1e2f23d4edc450d88975a3dc0f12ccc1c427c8a8 Mon Sep 17 00:00:00 2001 From: Vegard Skjefstad Date: Tue, 6 Dec 2022 18:27:41 +0000 Subject: [PATCH 042/232] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegi?= =?UTF-8?q?an=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (529 of 529 strings) Co-authored-by: Vegard Skjefstad Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/nb_NO/ Translation: Tusky/Tusky --- app/src/main/res/values-nb-rNO/strings.xml | 42 ++++++++++++++++------ 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index 4e6f59bd..f7fc5a84 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -106,7 +106,7 @@ Legg til fane Lenker Nevnelser - Stikkord + Emneknagger Åpne deler Vis delinger Vis favoritter @@ -140,14 +140,14 @@ Overskrift Hva er en instans\? Kobler til… - Addressen eller domenet til enhver instans kan legges til her, f. eks. mastodon.social, icosahedron.website, social.tchncs.de og mer! -\n -\nHvis du ikke har en konto kan du gi navnet på instansen du vil bli medlem av, og lage en konto der. -\n -\nEn instans er en plass der du har kontoen din, men du kan lett kommunisere med og følge andre personer på andre instanser, som om du var på den samme nettsiden. -\n + Addressen eller domenet til enhver instans kan legges til her, f. eks. mastodon.social, icosahedron.website, social.tchncs.de og mer! +\n +\nHvis du ikke har en konto kan du oppgi navnet på instansen du vil bli medlem av, og lage en konto der. +\n +\nEn instans der hvor du har kontoen din, men du kan lett kommunisere med og følge andre personer på andre instanser, som om du var på den samme nettsiden. +\n \nDu kan finne mer info på joinmastodon.org. - Opplasting av Media er ferdig + Opplasting av media er ferdig Laster opp… Last ned Trekk tilbake følgeforespørselen\? @@ -173,7 +173,7 @@ Tidslinjer Filtere Nettleser - Bruke Chrome-tilpassede faner + Bruk Chrome-tilpassede faner Skjul skriv-knappen ved scrolling Språk Tidslinjefiltrering @@ -250,7 +250,7 @@ Søk etter personer du følger Legg til konto i listen Fjern konto fra listen - Standardinstilling for innlegg + Standardinnstilling for innlegg Nye nevnelser Varsler om nye nevnelser Tusky er fri og åpen kildekode. Applikasjonen er lisensiert under GNU General Public License versjon 3. Du kan se lisensen her: https://www.gnu.org/licenses/gpl-3.0.en.html @@ -559,4 +559,26 @@ Klarte ikke å feste Klarte ikke å løsne Legg til reaksjon + Legg til eller fjern fra liste + Klarte ikke å legge til kontoen til listen + Klarte ikke å fjerne kontoen fra listen + Standardspråk på innlegg + Du har ingen lister. + %s (%s) + + Redigert %s + %s · %d innlegg vedlagt + Ny rapport på %s + %s rapporterte %s + Varsler om moderasjonsrapporter + det er en ny rapport + Rapporter + Redigert + Denne instansen støtter ikke følging av emneknagger. + Fulgte Emneknagger + Regelbrudd + Spam + Annet + Slutte å følge #%s\? + Sluttet å følge #%s \ No newline at end of file From e0beb115eb77a32cefdbeb3c51c78f9ba65d9214 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sveinn=20=C3=AD=20Felli?= Date: Tue, 6 Dec 2022 18:27:41 +0000 Subject: [PATCH 043/232] Translated using Weblate (Icelandic) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (531 of 531 strings) Translated using Weblate (Icelandic) Currently translated at 100.0% (529 of 529 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 | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index b19dcd1e..9471635e 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -559,4 +559,28 @@ Setja virknistað Aldrei Birta notandanafn á verkfærastikum + Bæta við eða fjarlægja af lista + Mistókst að bæta notandaaðgangnum á listann + Mistókst að fjarlægja notandaaðganginn af listanum + Þú ert ekki með neina lista. + Sjálfgefið tungumál færslna + %s (%s) + Þessi netþjónn styður ekki að fylgst sé með myllumerkjum. + Myllumerki sem fylgst er með + núna + Hætta að fylgjast með #%s\? + Ný kæra vegna %s + %s kærði %s + %s · %d færslur viðhengdar + Breytti %s + Hætt að fylgjast með #%s + Kærur + Tilkynningar um kærur umsjónarmanna + það er ný kæra + Breytti + Brot á reglu + Ruslpóstur + Annað + Villa við að þagga niður í #%s + Villa við að hætta að þagga niður í #%s \ No newline at end of file From 60b73a20e351943545ce130a62ac345c655c37df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=E1=BB=93=20Nh=E1=BA=A5t=20Duy?= Date: Tue, 6 Dec 2022 18:27:41 +0000 Subject: [PATCH 044/232] Translated using Weblate (Vietnamese) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (531 of 531 strings) Translated using Weblate (Vietnamese) Currently translated at 100.0% (529 of 529 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 | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 579c5869..9e4ba03c 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -564,4 +564,10 @@ Khác Máy chủ này không hỗ trợ theo dõi hashtag. Hashtag Theo Dõi + Đã sửa %s + Đã sửa + Ngôn ngữ đăng mặc định + %s (%s) + Lỗi khi ẩn #%s + Lỗi khi bỏ ẩn #%s \ No newline at end of file From 63cabccc545a42cf2533d5ec08d861c57a52945f Mon Sep 17 00:00:00 2001 From: puf Date: Tue, 6 Dec 2022 18:27:41 +0000 Subject: [PATCH 045/232] Translated using Weblate (Welsh) Currently translated at 83.0% (441 of 531 strings) Co-authored-by: puf Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/cy/ Translation: Tusky/Tusky --- app/src/main/res/values-cy/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-cy/strings.xml b/app/src/main/res/values-cy/strings.xml index 46df85c7..fbed3123 100644 --- a/app/src/main/res/values-cy/strings.xml +++ b/app/src/main/res/values-cy/strings.xml @@ -469,4 +469,5 @@ Ailfewngofnodwch i\'ch cyfrifon er mwyn galluogi hysbysiadau i\'ch ffôn. Er mwyn derbyn hysbysiadau i\'ch ffôn drwy UnifiedPush, mae angen caniatâd ar Tusky i danysgrifio i hysbysiadau ar eich gweinydd Mastodon. Bydd rhaid i chi fewngofnodi eto i newid y sgôp OAuth a roddir i Tusky. Bydd defnyddio\'r opsiwn ailfewngofnodi yma neu yn \'Dewisiadau\'ch cyfrif\' yn cadw\'ch holl ddrafftiau a\'ch storfa leol. Ydych chi\'n sicr yr hoffech chi rwystro %s i gyd\? Welwch chi ddim cynnwys o\'r parth hwnnw mewn unrhyw llinellau amser cyhoeddus nac ychwaith yn eich hysbysiadau. Ceir gwared ar eich dilynwyr o\'r parth hwnnw. + Anfeidrol \ No newline at end of file From 1a3e31036dd2d1b698fce65a4dd997d1b357dc14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Cmit=20Solmaz?= Date: Tue, 6 Dec 2022 18:27:41 +0000 Subject: [PATCH 046/232] Translated using Weblate (Turkish) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 82.4% (438 of 531 strings) Co-authored-by: Ümit Solmaz Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/tr/ Translation: Tusky/Tusky --- app/src/main/res/values-tr/strings.xml | 45 +++++++++++++++++++------- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index edd1d4db..9b2c64b1 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -9,21 +9,21 @@ Tanımlanamayan bir yetkilendirme hatası oluştu. Yetkilendirme reddedildi. Giriş belirteci alınırken hata oluştu. - Post çok uzun! + Yayın çok uzun! Bu tür bir dosya yüklenemez. Dosya açılamadı. Medya okuma izni gerekli. Medya kaydetme izni gerekli. Görüntüler ve videolar aynı duruma eklenemez. Yükleme başarısız oldu. - Post gönderirken hata. + Yayın gönderirken hata. Ana sayfa Bildirimler Yerel Federe - Direkt mesajlar + Doğrudan İletiler Sekmeler - Toot + Yayın Gönderiler Yanıtlarıyla Sabitlenmiş @@ -50,13 +50,13 @@ %s tootunuzu yineledi %s tootunuzu favorilerine ekledi %s seni takip etti - \@%s bildir + \@%s Bildir Daha fazla yorum? Hızlı yanıt Yanıtla Yinele Favorile - Daha fazla + Devamı Oluştur Mastodon ile giriş yap Oturumu kapat @@ -142,7 +142,7 @@ Kamu: Herkese açık ve sosyal çizelgelerinde çıkar Listelenmemiş: Genel zaman çizelgelerinde gösterme Özel: Sadece takipçiler ve bahsedilenlere açık - Direkt: Sadece bahsedilen kullanıcılara açık + Doğrudan: Sadece bahsedilen kullanıcılara açık Bildirimleri düzelt Anlık bildirimler Uyarılar @@ -353,7 +353,7 @@ Direkt Seçenekli anket: %1$s, %2$s, %3$s, %4$s; %5$s Liste adı - # olmadan hashtag + Etiket olmadan # Temizle Filtrele Uygula @@ -442,7 +442,7 @@ Hiç taslağınız yok. Zamanlanmış durumunuz yok. Kendi kitlenize yükseltin - Hashtags\'ler + Etiketler Boost etmeden önce onay iletişim kutusunu göster Bağlantı önizlemelerini zaman çizelgesinde göster Sekmeler arasında geçiş yapmak için kaydırma hareketini etkinleştir @@ -473,10 +473,31 @@ Üst araç çubuğunun başlığını gizle Konuşmayı sil Duyurular - Video ve ses dosyaları %s boyutunu aşamaz - Görüntü düzenlemedi + Video ve ses dosyaları %s MB boyutunu aşamaz + Görüntü düzenlemedi. #%s takip edilirken hata Giriş Hesap detayları alınırken hata - Giriş ekranı yüklenemedi + Giriş ekranı yüklenemedi. + Kapama hatası #%s + Takibi bırak #%s\? + İyilik + Kaydet! + Spam + Diğer + Daima + Birden fazla oturum açıldığında + Asla + Favorilerden önce onay iletişim kutusunu göster + Sesi açma hatası #%s + yeni bir rapor + Özel emojileri canlandır + %s rapor edilmiş %s + %s · %d ekli iletiler + Düzenleme %s + Bu hesap ile ilgili özel notunuz + Anlık bildirimler için yeniden giriş yapın + Takip Edilen Etiketler + %s kaydol + Yerimini kaldır \ No newline at end of file From 0d962c7cc12ce1ec90efabb6348bae79a0d36b78 Mon Sep 17 00:00:00 2001 From: Nik Clayton Date: Tue, 6 Dec 2022 20:23:48 +0100 Subject: [PATCH 047/232] Implement status() without rxjava (#2999) * Implement status() without rxjava * Use lambda param named `throwable` * Update DraftsActivity.kt --- .../tusky/components/drafts/DraftsActivity.kt | 23 +++++++++---------- .../components/drafts/DraftsViewModel.kt | 4 ++-- .../tusky/network/MastodonApi.kt | 4 ++-- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt index dfa36168..6665981c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt @@ -25,8 +25,7 @@ import androidx.activity.viewModels import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager -import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from -import autodispose2.autoDispose +import at.connyduck.calladapter.networkresult.fold import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.snackbar.Snackbar import com.keylesspalace.tusky.BaseActivity @@ -37,7 +36,6 @@ import com.keylesspalace.tusky.db.DraftEntity import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.util.parseAsMastodonHtml import com.keylesspalace.tusky.util.visible -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import retrofit2.HttpException @@ -88,13 +86,17 @@ class DraftsActivity : BaseActivity(), DraftActionListener { } override fun onOpenDraft(draft: DraftEntity) { + if (draft.inReplyToId == null) { + openDraftWithoutReply(draft) + return + } - if (draft.inReplyToId != null) { + val context = this as Context + + lifecycleScope.launch { bottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED viewModel.getStatus(draft.inReplyToId) - .observeOn(AndroidSchedulers.mainThread()) - .autoDispose(from(this)) - .subscribe( + .fold( { status -> val composeOptions = ComposeActivity.ComposeOptions( draftId = draft.id, @@ -113,10 +115,9 @@ class DraftsActivity : BaseActivity(), DraftActionListener { bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN - startActivity(ComposeActivity.startIntent(this, composeOptions)) + startActivity(ComposeActivity.startIntent(context, composeOptions)) }, { throwable -> - bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN Log.w(TAG, "failed loading reply information", throwable) @@ -124,7 +125,7 @@ class DraftsActivity : BaseActivity(), DraftActionListener { if (throwable is HttpException && throwable.code() == 404) { // the original status to which a reply was drafted has been deleted // let's open the ComposeActivity without reply information - Toast.makeText(this, getString(R.string.drafts_post_reply_removed), Toast.LENGTH_LONG).show() + Toast.makeText(context, getString(R.string.drafts_post_reply_removed), Toast.LENGTH_LONG).show() openDraftWithoutReply(draft) } else { Snackbar.make(binding.root, getString(R.string.drafts_failed_loading_reply), Snackbar.LENGTH_SHORT) @@ -132,8 +133,6 @@ class DraftsActivity : BaseActivity(), DraftActionListener { } } ) - } else { - openDraftWithoutReply(draft) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsViewModel.kt index 0c370222..69439803 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsViewModel.kt @@ -20,12 +20,12 @@ import androidx.lifecycle.viewModelScope import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.cachedIn +import at.connyduck.calladapter.networkresult.NetworkResult import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.db.DraftEntity import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.network.MastodonApi -import io.reactivex.rxjava3.core.Single import kotlinx.coroutines.launch import javax.inject.Inject @@ -60,7 +60,7 @@ class DraftsViewModel @Inject constructor( } } - fun getStatus(statusId: String): Single { + suspend fun getStatus(statusId: String): NetworkResult { return api.status(statusId) } 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 42652a12..9ddda4ba 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -165,9 +165,9 @@ interface MastodonApi { ): NetworkResult @GET("api/v1/statuses/{id}") - fun status( + suspend fun status( @Path("id") statusId: String - ): Single + ): NetworkResult @GET("api/v1/statuses/{id}") suspend fun statusAsync( From 08642d7bdb44b6d5881b119103f168434b05925f Mon Sep 17 00:00:00 2001 From: Nik Clayton Date: Tue, 6 Dec 2022 20:24:26 +0100 Subject: [PATCH 048/232] Use light/dark mode colors for image description text (#3003) * Use light/dark mode colors for image description text This is an accessibility issue -- in light mode (which should have dark text on a light background) the text color was hardcoded to light grey and the background color was a semi-transparent black. Fixes https://github.com/tuskyapp/Tusky/issues/2983. * Update app/src/main/res/drawable/ic_drag_indicator_horiz_24dp.xml Co-authored-by: Konrad Pozniak Co-authored-by: Konrad Pozniak --- app/src/main/res/drawable/description_bg_expanded.xml | 4 ++-- app/src/main/res/drawable/ic_drag_indicator_horiz_24dp.xml | 2 +- app/src/main/res/layout/fragment_view_image.xml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/drawable/description_bg_expanded.xml b/app/src/main/res/drawable/description_bg_expanded.xml index db2eebde..8a45cb7f 100644 --- a/app/src/main/res/drawable/description_bg_expanded.xml +++ b/app/src/main/res/drawable/description_bg_expanded.xml @@ -4,7 +4,7 @@ android:topLeftRadius="6dp" android:topRightRadius="6dp" /> - + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_drag_indicator_horiz_24dp.xml b/app/src/main/res/drawable/ic_drag_indicator_horiz_24dp.xml index eb611224..e8be67fe 100644 --- a/app/src/main/res/drawable/ic_drag_indicator_horiz_24dp.xml +++ b/app/src/main/res/drawable/ic_drag_indicator_horiz_24dp.xml @@ -4,6 +4,6 @@ - + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_view_image.xml b/app/src/main/res/layout/fragment_view_image.xml index 93cf2def..e0bc90a8 100644 --- a/app/src/main/res/layout/fragment_view_image.xml +++ b/app/src/main/res/layout/fragment_view_image.xml @@ -70,7 +70,7 @@ android:paddingRight="8dp" android:paddingBottom="8dp" android:textAlignment="center" - android:textColor="#eee" + android:textColor="?android:textColorPrimary" android:textSize="?attr/status_text_medium" tools:text="Some media description which might get quite long so that it won't easily fit in one line" /> From 48ad2f9eee78f173e098f8b93b27a0394c1d13c4 Mon Sep 17 00:00:00 2001 From: Nik Clayton Date: Tue, 6 Dec 2022 20:31:16 +0100 Subject: [PATCH 049/232] Ensure the "Apply" button is always visible (#3004) On smaller devices the notification filter listview may be longer than the screen height, and pushes the "Apply" button out of sight. Fix this by: - Set the height of the listview to 0dp and its layout_weight to 1 - Set the layout_weight of the button to 0 This ensures the button always appears (because the listview height is 0dp) and the listview then expands to fill any remaining space (because the layout_weight is 1). Fixes https://github.com/tuskyapp/Tusky/issues/2985 --- app/src/main/res/layout/notifications_filter.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/layout/notifications_filter.xml b/app/src/main/res/layout/notifications_filter.xml index 977b542e..76100f21 100644 --- a/app/src/main/res/layout/notifications_filter.xml +++ b/app/src/main/res/layout/notifications_filter.xml @@ -6,12 +6,14 @@ + android:layout_height="0dp" + android:layout_weight="1" />