diff --git a/app/build.gradle b/app/build.gradle index 7e646115..fbc6d6f5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,7 +1,7 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' +apply plugin: 'kotlin-parcelize' apply from: "../instance-build.gradle" @@ -20,8 +20,8 @@ android { applicationId APP_ID minSdkVersion 21 targetSdkVersion 29 - versionCode 80 - versionName "14.0-CW2" + versionCode 81 + versionName "15.1-CW1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true @@ -65,9 +65,6 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } - androidExtensions { - experimental = true - } buildFeatures { viewBinding true } @@ -101,7 +98,7 @@ project.tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { } ext.lifecycleVersion = "2.2.0" -ext.roomVersion = '2.2.5' +ext.roomVersion = '2.3.0' ext.retrofitVersion = '2.9.0' ext.okhttpVersion = '4.9.0' ext.glideVersion = '4.11.0' @@ -117,7 +114,7 @@ dependencies { implementation "androidx.fragment:fragment-ktx:1.2.5" implementation "androidx.browser:browser:1.3.0" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" - implementation "androidx.recyclerview:recyclerview:1.1.0" + implementation "androidx.recyclerview:recyclerview:1.2.0" implementation "androidx.exifinterface:exifinterface:1.3.2" implementation "androidx.cardview:cardview:1.0.0" implementation "androidx.preference:preference-ktx:1.1.1" @@ -126,6 +123,7 @@ dependencies { implementation "androidx.emoji:emoji-appcompat:1.1.0" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion" + implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-reactivestreams-ktx:$lifecycleVersion" implementation "androidx.constraintlayout:constraintlayout:2.0.4" implementation "androidx.paging:paging-runtime-ktx:2.1.2" @@ -135,7 +133,7 @@ dependencies { implementation "androidx.room:room-rxjava2:$roomVersion" kapt "androidx.room:room-compiler:$roomVersion" - implementation "com.google.android.material:material:1.2.1" + implementation "com.google.android.material:material:1.3.0" implementation "com.squareup.retrofit2:retrofit:$retrofitVersion" implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion" @@ -143,7 +141,6 @@ dependencies { implementation "com.squareup.okhttp3:okhttp:$okhttpVersion" implementation "com.squareup.okhttp3:logging-interceptor:$okhttpVersion" - implementation "com.squareup.okhttp3:okhttp-tls:$okhttpVersion" implementation "org.conscrypt:conscrypt-android:2.5.1" diff --git a/app/src/main/java/com/keylesspalace/tusky/AboutActivity.kt b/app/src/main/java/com/keylesspalace/tusky/AboutActivity.kt index 48095425..ada7af36 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AboutActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/AboutActivity.kt @@ -9,19 +9,20 @@ import android.text.method.LinkMovementMethod import android.text.style.URLSpan import android.text.util.Linkify import android.widget.TextView +import com.keylesspalace.tusky.databinding.ActivityAboutBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.util.CustomURLSpan import com.keylesspalace.tusky.util.hide -import kotlinx.android.synthetic.main.activity_about.* -import kotlinx.android.synthetic.main.toolbar_basic.* class AboutActivity : BottomSheetActivity(), Injectable { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_about) - setSupportActionBar(toolbar) + val binding = ActivityAboutBinding.inflate(layoutInflater) + setContentView(binding.root) + + setSupportActionBar(binding.includedToolbar.toolbar) supportActionBar?.run { setDisplayHomeAsUpEnabled(true) setDisplayShowHomeEnabled(true) @@ -29,26 +30,24 @@ class AboutActivity : BottomSheetActivity(), Injectable { setTitle(R.string.about_title_activity) - versionTextView.text = getString(R.string.about_app_version, getString(R.string.app_name), BuildConfig.VERSION_NAME) + binding.versionTextView.text = getString(R.string.about_app_version, getString(R.string.app_name), BuildConfig.VERSION_NAME) if(BuildConfig.CUSTOM_INSTANCE.isBlank()) { - aboutPoweredByTusky.hide() + binding.aboutPoweredByTusky.hide() } - aboutLicenseInfoTextView.setClickableTextWithoutUnderlines(R.string.about_tusky_license) - aboutWebsiteInfoTextView.setClickableTextWithoutUnderlines(R.string.about_project_site) - aboutBugsFeaturesInfoTextView.setClickableTextWithoutUnderlines(R.string.about_bug_feature_request_site) + binding.aboutLicenseInfoTextView.setClickableTextWithoutUnderlines(R.string.about_tusky_license) + binding.aboutWebsiteInfoTextView.setClickableTextWithoutUnderlines(R.string.about_project_site) + binding.aboutBugsFeaturesInfoTextView.setClickableTextWithoutUnderlines(R.string.about_bug_feature_request_site) - tuskyProfileButton.setOnClickListener { + binding.tuskyProfileButton.setOnClickListener { viewUrl(BuildConfig.SUPPORT_ACCOUNT_URL) } - aboutLicensesButton.setOnClickListener { + binding.aboutLicensesButton.setOnClickListener { startActivityWithSlideInAnimation(Intent(this, LicenseActivity::class.java)) } - } - } private fun TextView.setClickableTextWithoutUnderlines(@StringRes textId: Int) { @@ -73,5 +72,4 @@ private fun TextView.setClickableTextWithoutUnderlines(@StringRes textId: Int) { setText(builder) linksClickable = true movementMethod = LinkMovementMethod.getInstance() - } diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt index 14c5737e..8f2f7188 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt @@ -50,6 +50,7 @@ import com.google.android.material.tabs.TabLayoutMediator import com.keylesspalace.tusky.adapter.AccountFieldAdapter import com.keylesspalace.tusky.components.compose.ComposeActivity import com.keylesspalace.tusky.components.report.ReportActivity +import com.keylesspalace.tusky.databinding.ActivityAccountBinding import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Relationship @@ -63,8 +64,6 @@ import com.keylesspalace.tusky.view.showMuteAccountDialog import com.keylesspalace.tusky.viewmodel.AccountViewModel import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector -import kotlinx.android.synthetic.main.activity_account.* -import kotlinx.android.synthetic.main.view_account_moved.* import java.text.NumberFormat import javax.inject.Inject import kotlin.math.abs @@ -78,6 +77,8 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI private val viewModel: AccountViewModel by viewModels { viewModelFactory } + private val binding: ActivityAccountBinding by viewBinding(ActivityAccountBinding::inflate) + private lateinit var accountFieldAdapter : AccountFieldAdapter private var followState: FollowState = FollowState.NOT_FOLLOWING @@ -118,7 +119,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI super.onCreate(savedInstanceState) loadResources() makeNotificationBarTransparent() - setContentView(R.layout.activity_account) + setContentView(binding.root) // Obtain information to fill out the profile. viewModel.setAccountInfo(intent.getStringExtra(KEY_ACCOUNT_ID)!!) @@ -136,9 +137,9 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI if (viewModel.isSelf) { updateButtons() - saveNoteInfo.hide() + binding.saveNoteInfo.hide() } else { - saveNoteInfo.visibility = View.INVISIBLE + binding.saveNoteInfo.visibility = View.INVISIBLE } } @@ -158,16 +159,16 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI */ private fun setupAccountViews() { // Initialise the default UI states. - accountFloatingActionButton.hide() - accountFollowButton.hide() - accountMuteButton.hide() - accountFollowsYouTextView.hide() + binding.accountFloatingActionButton.hide() + binding.accountFollowButton.hide() + binding.accountMuteButton.hide() + binding.accountFollowsYouTextView.hide() // setup the RecyclerView for the account fields accountFieldAdapter = AccountFieldAdapter(this, animateEmojis) - accountFieldList.isNestedScrollingEnabled = false - accountFieldList.layoutManager = LinearLayoutManager(this) - accountFieldList.adapter = accountFieldAdapter + binding.accountFieldList.isNestedScrollingEnabled = false + binding.accountFieldList.layoutManager = LinearLayoutManager(this) + binding.accountFieldList.adapter = accountFieldAdapter val accountListClickListener = { v: View -> @@ -179,15 +180,15 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI val accountListIntent = AccountListActivity.newIntent(this, type, viewModel.accountId) startActivityWithSlideInAnimation(accountListIntent) } - accountFollowers.setOnClickListener(accountListClickListener) - accountFollowing.setOnClickListener(accountListClickListener) + binding.accountFollowers.setOnClickListener(accountListClickListener) + binding.accountFollowing.setOnClickListener(accountListClickListener) - accountStatuses.setOnClickListener { + binding.accountStatuses.setOnClickListener { // Make nice ripple effect on tab - accountTabLayout.getTabAt(0)!!.select() - val poorTabView = (accountTabLayout.getChildAt(0) as ViewGroup).getChildAt(0) + binding.accountTabLayout.getTabAt(0)!!.select() + val poorTabView = (binding.accountTabLayout.getChildAt(0) as ViewGroup).getChildAt(0) poorTabView.isPressed = true - accountTabLayout.postDelayed({ poorTabView.isPressed = false }, 300) + binding.accountTabLayout.postDelayed({ poorTabView.isPressed = false }, 300) } // If wellbeing mode is enabled, follow stats and posts count should be hidden @@ -195,11 +196,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI val wellbeingEnabled = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_PROFILE, false) if (wellbeingEnabled) { - accountStatuses.hide() - accountFollowers.hide() - accountFollowing.hide() + binding.accountStatuses.hide() + binding.accountFollowers.hide() + binding.accountFollowing.hide() } - } /** @@ -209,19 +209,19 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI // Setup the tabs and timeline pager. adapter = AccountPagerAdapter(this, viewModel.accountId) - accountFragmentViewPager.adapter = adapter - accountFragmentViewPager.offscreenPageLimit = 2 + binding.accountFragmentViewPager.adapter = adapter + binding.accountFragmentViewPager.offscreenPageLimit = 2 val pageTitles = arrayOf(getString(R.string.title_statuses), getString(R.string.title_statuses_with_replies), getString(R.string.title_statuses_pinned), getString(R.string.title_media)) - TabLayoutMediator(accountTabLayout, accountFragmentViewPager) { tab, position -> + TabLayoutMediator(binding.accountTabLayout, binding.accountFragmentViewPager) { tab, position -> tab.text = pageTitles[position] }.attach() val pageMargin = resources.getDimensionPixelSize(R.dimen.tab_page_margin) - accountFragmentViewPager.setPageTransformer(MarginPageTransformer(pageMargin)) + binding.accountFragmentViewPager.setPageTransformer(MarginPageTransformer(pageMargin)) - accountTabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { + binding.accountTabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { override fun onTabReselected(tab: TabLayout.Tab?) { tab?.position?.let { position -> (adapter.getFragment(position) as? ReselectableFragment)?.onReselect() @@ -237,17 +237,17 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI private fun setupToolbar() { // set toolbar top margin according to system window insets - accountCoordinatorLayout.setOnApplyWindowInsetsListener { _, insets -> + binding.accountCoordinatorLayout.setOnApplyWindowInsetsListener { _, insets -> val top = insets.systemWindowInsetTop - val toolbarParams = accountToolbar.layoutParams as CollapsingToolbarLayout.LayoutParams + val toolbarParams = binding.accountToolbar.layoutParams as CollapsingToolbarLayout.LayoutParams toolbarParams.topMargin = top insets.consumeSystemWindowInsets() } // Setup the toolbar. - setSupportActionBar(accountToolbar) + setSupportActionBar(binding.accountToolbar) supportActionBar?.run { setDisplayHomeAsUpEnabled(true) setDisplayShowHomeEnabled(true) @@ -258,9 +258,9 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI val toolbarBackground = MaterialShapeDrawable.createWithElevationOverlay(this, appBarElevation) toolbarBackground.fillColor = ColorStateList.valueOf(Color.TRANSPARENT) - accountToolbar.background = toolbarBackground + binding.accountToolbar.background = toolbarBackground - accountHeaderInfoContainer.background = MaterialShapeDrawable.createWithElevationOverlay(this, appBarElevation) + binding.accountHeaderInfoContainer.background = MaterialShapeDrawable.createWithElevationOverlay(this, appBarElevation) val avatarBackground = MaterialShapeDrawable.createWithElevationOverlay(this, appBarElevation).apply { fillColor = ColorStateList.valueOf(toolbarColor) @@ -269,10 +269,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI .setAllCornerSizes(resources.getDimension(R.dimen.account_avatar_background_radius)) .build() } - accountAvatarImageView.background = avatarBackground + binding.accountAvatarImageView.background = avatarBackground // Add a listener to change the toolbar icon color when it enters/exits its collapsed state. - accountAppBarLayout.addOnOffsetChangedListener(object : AppBarLayout.OnOffsetChangedListener { + binding.accountAppBarLayout.addOnOffsetChangedListener(object : AppBarLayout.OnOffsetChangedListener { override fun onOffsetChanged(appBarLayout: AppBarLayout, verticalOffset: Int) { @@ -289,19 +289,19 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI if (hideFab && !viewModel.isSelf && !blocking) { if (verticalOffset > oldOffset) { - accountFloatingActionButton.show() + binding.accountFloatingActionButton.show() } if (verticalOffset < oldOffset) { - accountFloatingActionButton.hide() + binding.accountFloatingActionButton.hide() } } val scaledAvatarSize = (avatarSize + verticalOffset) / avatarSize - accountAvatarImageView.scaleX = scaledAvatarSize - accountAvatarImageView.scaleY = scaledAvatarSize + binding.accountAvatarImageView.scaleX = scaledAvatarSize + binding.accountAvatarImageView.scaleY = scaledAvatarSize - accountAvatarImageView.visible(scaledAvatarSize > 0) + binding.accountAvatarImageView.visible(scaledAvatarSize > 0) val transparencyPercent = (abs(verticalOffset) / titleVisibleHeight.toFloat()).coerceAtMost(1f) @@ -311,7 +311,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI toolbarBackground.fillColor = ColorStateList.valueOf(evaluatedToolbarColor) - swipeToRefreshLayout.isEnabled = verticalOffset == 0 + binding.swipeToRefreshLayout.isEnabled = verticalOffset == 0 } }) @@ -331,7 +331,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI when (it) { is Success -> onAccountChanged(it.data) is Error -> { - Snackbar.make(accountCoordinatorLayout, R.string.error_generic, Snackbar.LENGTH_LONG) + Snackbar.make(binding.accountCoordinatorLayout, R.string.error_generic, Snackbar.LENGTH_LONG) .setAction(R.string.action_retry) { viewModel.refresh() } .show() } @@ -344,7 +344,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI } if (it is Error) { - Snackbar.make(accountCoordinatorLayout, R.string.error_generic, Snackbar.LENGTH_LONG) + Snackbar.make(binding.accountCoordinatorLayout, R.string.error_generic, Snackbar.LENGTH_LONG) .setAction(R.string.action_retry) { viewModel.refresh() } .show() } @@ -355,7 +355,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI accountFieldAdapter.notifyDataSetChanged() }) viewModel.noteSaved.observe(this) { - saveNoteInfo.visible(it, View.INVISIBLE) + binding.saveNoteInfo.visible(it, View.INVISIBLE) } } @@ -363,32 +363,32 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI * Setup swipe to refresh layout */ private fun setupRefreshLayout() { - swipeToRefreshLayout.setOnRefreshListener { + binding.swipeToRefreshLayout.setOnRefreshListener { viewModel.refresh() adapter.refreshContent() } viewModel.isRefreshing.observe(this, { isRefreshing -> - swipeToRefreshLayout.isRefreshing = isRefreshing == true + binding.swipeToRefreshLayout.isRefreshing = isRefreshing == true }) - swipeToRefreshLayout.setColorSchemeResources(R.color.chinwag_green) + binding.swipeToRefreshLayout.setColorSchemeResources(R.color.chinwag_green) } private fun onAccountChanged(account: Account?) { loadedAccount = account ?: return val usernameFormatted = getString(R.string.status_username_format, account.username) - accountUsernameTextView.text = usernameFormatted - accountDisplayNameTextView.text = account.name.emojify(account.emojis, accountDisplayNameTextView, animateEmojis) + binding.accountUsernameTextView.text = usernameFormatted + binding.accountDisplayNameTextView.text = account.name.emojify(account.emojis, binding.accountDisplayNameTextView, animateEmojis) - val emojifiedNote = account.note.emojify(account.emojis, accountNoteTextView, animateEmojis) - LinkHelper.setClickableText(accountNoteTextView, emojifiedNote, null, this) + val emojifiedNote = account.note.emojify(account.emojis, binding.accountNoteTextView, animateEmojis) + LinkHelper.setClickableText(binding.accountNoteTextView, emojifiedNote, null, this) // accountFieldAdapter.fields = account.fields ?: emptyList() accountFieldAdapter.emojis = account.emojis ?: emptyList() accountFieldAdapter.notifyDataSetChanged() - accountLockedImageView.visible(account.locked) - accountBadgeTextView.visible(account.bot) + binding.accountLockedImageView.visible(account.locked) + binding.accountBadgeTextView.visible(account.bot) updateAccountAvatar() updateToolbar() @@ -397,7 +397,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI updateAccountStats() invalidateOptionsMenu() - accountMuteButton.setOnClickListener { + binding.accountMuteButton.setOnClickListener { viewModel.unmuteAccount() updateMuteButton() } @@ -411,7 +411,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI loadAvatar( account.avatar, - accountAvatarImageView, + binding.accountAvatarImageView, resources.getDimensionPixelSize(R.dimen.avatar_radius_94dp), animateAvatar ) @@ -420,10 +420,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI .asBitmap() .load(account.header) .centerCrop() - .into(accountHeaderImageView) + .into(binding.accountHeaderImageView) - accountAvatarImageView.setOnClickListener { avatarView -> + binding.accountAvatarImageView.setOnClickListener { avatarView -> val intent = ViewMediaActivity.newSingleImageIntent(avatarView.context, account.avatar) avatarView.transitionName = account.avatar @@ -440,7 +440,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI private fun updateToolbar() { loadedAccount?.let { account -> - val emojifiedName = account.name.emojify(account.emojis, accountToolbar, animateEmojis) + val emojifiedName = account.name.emojify(account.emojis, binding.accountToolbar, animateEmojis) try { supportActionBar?.title = EmojiCompat.get().process(emojifiedName) @@ -457,28 +457,27 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI private fun updateMovedAccount() { loadedAccount?.moved?.let { movedAccount -> - accountMovedView?.show() + binding.accountMovedView.show() - // necessary because accountMovedView is now replaced in layout hierachy - findViewById(R.id.accountMovedViewLayout).setOnClickListener { + binding.accountMovedView.setOnClickListener { onViewAccount(movedAccount.id) } - accountMovedDisplayName.text = movedAccount.name - accountMovedUsername.text = getString(R.string.status_username_format, movedAccount.username) + binding.accountMovedDisplayName.text = movedAccount.name + binding.accountMovedUsername.text = getString(R.string.status_username_format, movedAccount.username) val avatarRadius = resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp) - loadAvatar(movedAccount.avatar, accountMovedAvatar, avatarRadius, animateAvatar) + loadAvatar(movedAccount.avatar, binding.accountMovedAvatar, avatarRadius, animateAvatar) - accountMovedText.text = getString(R.string.account_moved_description, movedAccount.name) + binding.accountMovedText.text = getString(R.string.account_moved_description, movedAccount.name) // this is necessary because API 19 can't handle vector compound drawables val movedIcon = ContextCompat.getDrawable(this, R.drawable.ic_briefcase)?.mutate() val textColor = ThemeUtils.getColor(this, android.R.attr.textColorTertiary) movedIcon?.colorFilter = PorterDuffColorFilter(textColor, PorterDuff.Mode.SRC_IN) - accountMovedText.setCompoundDrawablesRelativeWithIntrinsicBounds(movedIcon, null, null, null) + binding.accountMovedText.setCompoundDrawablesRelativeWithIntrinsicBounds(movedIcon, null, null, null) } } @@ -489,8 +488,8 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI private fun updateRemoteAccount() { loadedAccount?.let { account -> if (account.isRemote()) { - accountRemoveView.show() - accountRemoveView.setOnClickListener { + binding.accountRemoveView.show() + binding.accountRemoveView.setOnClickListener { LinkHelper.openLink(account.url, this) } } @@ -503,13 +502,13 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI private fun updateAccountStats() { loadedAccount?.let { account -> val numberFormat = NumberFormat.getNumberInstance() - accountFollowersTextView.text = numberFormat.format(account.followersCount) - accountFollowingTextView.text = numberFormat.format(account.followingCount) - accountStatusesTextView.text = numberFormat.format(account.statusesCount) + binding.accountFollowersTextView.text = numberFormat.format(account.followersCount) + binding.accountFollowingTextView.text = numberFormat.format(account.followingCount) + binding.accountStatusesTextView.text = numberFormat.format(account.statusesCount) - accountFloatingActionButton.setOnClickListener { mention() } + binding.accountFloatingActionButton.setOnClickListener { mention() } - accountFollowButton.setOnClickListener { + binding.accountFollowButton.setOnClickListener { if (viewModel.isSelf) { val intent = Intent(this@AccountActivity, EditProfileActivity::class.java) startActivity(intent) @@ -552,14 +551,14 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI val preferences = PreferenceManager.getDefaultSharedPreferences(this) val wellbeingEnabled = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_PROFILE, false) - accountFollowsYouTextView.visible(relation.followedBy && !wellbeingEnabled) + binding.accountFollowsYouTextView.visible(relation.followedBy && !wellbeingEnabled) // because subscribing is Pleroma extension, enable it __only__ when we have non-null subscribing field // it's also now supported in Mastodon 3.3.0rc but called notifying and use different API call if(!viewModel.isSelf && followState == FollowState.FOLLOWING && (relation.subscribing != null || relation.notifying != null)) { - accountSubscribeButton.show() - accountSubscribeButton.setOnClickListener { + binding.accountSubscribeButton.show() + binding.accountSubscribeButton.setOnClickListener { viewModel.changeSubscribingState() } if(relation.notifying != null) @@ -569,12 +568,12 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI } // remove the listener so it doesn't fire on non-user changes - accountNoteTextInputLayout.editText?.removeTextChangedListener(noteWatcher) + binding.accountNoteTextInputLayout.editText?.removeTextChangedListener(noteWatcher) - accountNoteTextInputLayout.visible(relation.note != null) - accountNoteTextInputLayout.editText?.setText(relation.note) + binding.accountNoteTextInputLayout.visible(relation.note != null) + binding.accountNoteTextInputLayout.editText?.setText(relation.note) - accountNoteTextInputLayout.editText?.addTextChangedListener(noteWatcher) + binding.accountNoteTextInputLayout.editText?.addTextChangedListener(noteWatcher) updateButtons() } @@ -587,22 +586,22 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI private fun updateFollowButton() { if (viewModel.isSelf) { - accountFollowButton.setText(R.string.action_edit_own_profile) + binding.accountFollowButton.setText(R.string.action_edit_own_profile) return } if (blocking) { - accountFollowButton.setText(R.string.action_unblock) + binding.accountFollowButton.setText(R.string.action_unblock) return } when (followState) { FollowState.NOT_FOLLOWING -> { - accountFollowButton.setText(R.string.action_follow) + binding.accountFollowButton.setText(R.string.action_follow) } FollowState.REQUESTED -> { - accountFollowButton.setText(R.string.state_follow_requested) + binding.accountFollowButton.setText(R.string.state_follow_requested) } FollowState.FOLLOWING -> { - accountFollowButton.setText(R.string.action_unfollow) + binding.accountFollowButton.setText(R.string.action_unfollow) } } updateSubscribeButton() @@ -610,23 +609,23 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI private fun updateMuteButton() { if (muting) { - accountMuteButton.setIconResource(R.drawable.ic_unmute_24dp) + binding.accountMuteButton.setIconResource(R.drawable.ic_unmute_24dp) } else { - accountMuteButton.hide() + binding.accountMuteButton.hide() } } private fun updateSubscribeButton() { if(followState != FollowState.FOLLOWING) { - accountSubscribeButton.hide() + binding.accountSubscribeButton.hide() } if(subscribing) { - accountSubscribeButton.setIconResource(R.drawable.ic_notifications_active_24dp) - accountSubscribeButton.contentDescription = getString(R.string.action_unsubscribe_account) + binding.accountSubscribeButton.setIconResource(R.drawable.ic_notifications_active_24dp) + binding.accountSubscribeButton.contentDescription = getString(R.string.action_unsubscribe_account) } else { - accountSubscribeButton.setIconResource(R.drawable.ic_notifications_24dp) - accountSubscribeButton.contentDescription = getString(R.string.action_subscribe_account) + binding.accountSubscribeButton.setIconResource(R.drawable.ic_notifications_24dp) + binding.accountSubscribeButton.contentDescription = getString(R.string.action_subscribe_account) } } @@ -635,27 +634,27 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI if (loadedAccount?.moved == null) { - accountFollowButton.show() + binding.accountFollowButton.show() updateFollowButton() if (blocking || viewModel.isSelf) { - accountFloatingActionButton.hide() - accountMuteButton.hide() - accountSubscribeButton.hide() + binding.accountFloatingActionButton.hide() + binding.accountMuteButton.hide() + binding.accountSubscribeButton.hide() } else { - accountFloatingActionButton.show() + binding.accountFloatingActionButton.show() if (muting) - accountMuteButton.show() + binding.accountMuteButton.show() else - accountMuteButton.hide() + binding.accountMuteButton.hide() updateMuteButton() } } else { - accountFloatingActionButton.hide() - accountFollowButton.hide() - accountMuteButton.hide() - accountSubscribeButton.hide() + binding.accountFloatingActionButton.hide() + binding.accountFollowButton.hide() + binding.accountMuteButton.hide() + binding.accountSubscribeButton.hide() } } @@ -833,7 +832,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI override fun getActionButton(): FloatingActionButton? { return if (!viewModel.isSelf && !blocking) { - accountFloatingActionButton + binding.accountFloatingActionButton } else null } diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountListActivity.kt b/app/src/main/java/com/keylesspalace/tusky/AccountListActivity.kt index d592f053..7f00150f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountListActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/AccountListActivity.kt @@ -18,10 +18,10 @@ package com.keylesspalace.tusky import android.content.Context import android.content.Intent import android.os.Bundle +import com.keylesspalace.tusky.databinding.ActivityAccountListBinding import com.keylesspalace.tusky.fragment.AccountListFragment import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector -import kotlinx.android.synthetic.main.toolbar_basic.* import javax.inject.Inject class AccountListActivity : BaseActivity(), HasAndroidInjector { @@ -41,12 +41,14 @@ class AccountListActivity : BaseActivity(), HasAndroidInjector { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_account_list) + val binding = ActivityAccountListBinding.inflate(layoutInflater) + setContentView(binding.root) val type = intent.getSerializableExtra(EXTRA_TYPE) as Type val id: String? = intent.getStringExtra(EXTRA_ID) + val accountLocked: Boolean = intent.getBooleanExtra(EXTRA_ACCOUNT_LOCKED, false) - setSupportActionBar(toolbar) + setSupportActionBar(binding.includedToolbar.toolbar) supportActionBar?.apply { when (type) { Type.BLOCKS -> setTitle(R.string.title_blocks) @@ -63,7 +65,7 @@ class AccountListActivity : BaseActivity(), HasAndroidInjector { supportFragmentManager .beginTransaction() - .replace(R.id.fragment_container, AccountListFragment.newInstance(type, id)) + .replace(R.id.fragment_container, AccountListFragment.newInstance(type, id, accountLocked)) .commit() } @@ -72,12 +74,15 @@ class AccountListActivity : BaseActivity(), HasAndroidInjector { companion object { private const val EXTRA_TYPE = "type" private const val EXTRA_ID = "id" + private const val EXTRA_ACCOUNT_LOCKED = "acc_locked" @JvmStatic - fun newIntent(context: Context, type: Type, id: String? = null): Intent { + @JvmOverloads + fun newIntent(context: Context, type: Type, id: String? = null, accountLocked: Boolean = false): Intent { return Intent(context, AccountListActivity::class.java).apply { putExtra(EXTRA_TYPE, type) putExtra(EXTRA_ID, id) + putExtra(EXTRA_ACCOUNT_LOCKED, accountLocked) } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt b/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt index f1c3d54d..df381aa1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt @@ -23,11 +23,13 @@ import android.view.ViewGroup import android.widget.LinearLayout import androidx.appcompat.widget.SearchView import androidx.fragment.app.DialogFragment +import androidx.fragment.app.viewModels import androidx.preference.PreferenceManager import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView +import com.keylesspalace.tusky.databinding.FragmentAccountsInListBinding +import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.entity.Account @@ -38,9 +40,6 @@ import com.keylesspalace.tusky.viewmodel.State import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from import com.uber.autodispose.autoDispose import io.reactivex.android.schedulers.AndroidSchedulers -import kotlinx.android.extensions.LayoutContainer -import kotlinx.android.synthetic.main.fragment_accounts_in_list.* -import kotlinx.android.synthetic.main.item_follow_request.* import java.io.IOException import javax.inject.Inject @@ -48,23 +47,11 @@ private typealias AccountInfo = Pair class AccountsInListFragment : DialogFragment(), Injectable { - companion object { - private const val LIST_ID_ARG = "listId" - private const val LIST_NAME_ARG = "listName" - - @JvmStatic - fun newInstance(listId: String, listName: String): AccountsInListFragment { - val args = Bundle().apply { - putString(LIST_ID_ARG, listId) - putString(LIST_NAME_ARG, listName) - } - return AccountsInListFragment().apply { arguments = args } - } - } - @Inject lateinit var viewModelFactory: ViewModelFactory - lateinit var viewModel: AccountsInListViewModel + + private val viewModel: AccountsInListViewModel by viewModels { viewModelFactory } + private val binding by viewBinding(FragmentAccountsInListBinding::bind) private lateinit var listId: String private lateinit var listName: String @@ -79,7 +66,6 @@ class AccountsInListFragment : DialogFragment(), Injectable { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setStyle(STYLE_NORMAL, R.style.TuskyDialogFragmentStyle) - viewModel = viewModelFactory.create(AccountsInListViewModel::class.java) val args = requireArguments() listId = args.getString(LIST_ID_ARG)!! listName = args.getString(LIST_NAME_ARG)!! @@ -100,12 +86,11 @@ class AccountsInListFragment : DialogFragment(), Injectable { } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - accountsRecycler.layoutManager = LinearLayoutManager(view.context) - accountsRecycler.adapter = adapter + binding.accountsRecycler.layoutManager = LinearLayoutManager(view.context) + binding.accountsRecycler.adapter = adapter - accountsSearchRecycler.layoutManager = LinearLayoutManager(view.context) - accountsSearchRecycler.adapter = searchAdapter + binding.accountsSearchRecycler.layoutManager = LinearLayoutManager(view.context) + binding.accountsSearchRecycler.adapter = searchAdapter viewModel.state .observeOn(AndroidSchedulers.mainThread()) @@ -114,15 +99,15 @@ class AccountsInListFragment : DialogFragment(), Injectable { adapter.submitList(state.accounts.asRightOrNull() ?: listOf()) when (state.accounts) { - is Either.Right -> messageView.hide() + is Either.Right -> binding.messageView.hide() is Either.Left -> handleError(state.accounts.value) } setupSearchView(state) } - searchView.isSubmitButtonEnabled = true - searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + binding.searchView.isSubmitButtonEnabled = true + binding.searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String?): Boolean { viewModel.search(query ?: "") return true @@ -141,30 +126,30 @@ class AccountsInListFragment : DialogFragment(), Injectable { private fun setupSearchView(state: State) { if (state.searchResult == null) { searchAdapter.submitList(listOf()) - accountsSearchRecycler.hide() - accountsRecycler.show() + binding.accountsSearchRecycler.hide() + binding.accountsRecycler.show() } else { val listAccounts = state.accounts.asRightOrNull() ?: listOf() val newList = state.searchResult.map { acc -> acc to listAccounts.contains(acc) } searchAdapter.submitList(newList) - accountsSearchRecycler.show() - accountsRecycler.hide() + binding.accountsSearchRecycler.show() + binding.accountsRecycler.hide() } } private fun handleError(error: Throwable) { - messageView.show() + binding.messageView.show() val retryAction = { _: View -> - messageView.hide() + binding.messageView.hide() viewModel.load(listId) } if (error is IOException) { - messageView.setup(R.drawable.elephant_offline, + binding.messageView.setup(R.drawable.elephant_offline, R.string.error_network, retryAction) } else { - messageView.setup(R.drawable.elephant_error, + binding.messageView.setup(R.drawable.elephant_error, R.string.error_generic, retryAction) } } @@ -187,39 +172,28 @@ class AccountsInListFragment : DialogFragment(), Injectable { } } - inner class Adapter : ListAdapter(AccountDiffer) { + inner class Adapter : ListAdapter>(AccountDiffer) { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val view = LayoutInflater.from(parent.context) - .inflate(R.layout.item_follow_request, parent, false) - return ViewHolder(view) + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder { + val binding = ItemFollowRequestBinding.inflate(LayoutInflater.from(parent.context), parent, false) + val holder = BindingHolder(binding) + + binding.notificationTextView.hide() + binding.acceptButton.hide() + binding.rejectButton.setOnClickListener { + onRemoveFromList(getItem(holder.bindingAdapterPosition).id) + } + binding.rejectButton.contentDescription = + binding.root.context.getString(R.string.action_remove_from_list) + + return holder } - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - holder.bind(getItem(position)) - } - - inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), - View.OnClickListener, LayoutContainer { - - override val containerView = itemView - - init { - acceptButton.hide() - rejectButton.setOnClickListener(this) - rejectButton.contentDescription = - itemView.context.getString(R.string.action_remove_from_list) - } - - fun bind(account: Account) { - displayNameTextView.text = account.name.emojify(account.emojis, displayNameTextView, animateEmojis) - usernameTextView.text = account.username - loadAvatar(account.avatar, avatar, radius, animateAvatar) - } - - override fun onClick(v: View?) { - onRemoveFromList(getItem(adapterPosition).id) - } + override fun onBindViewHolder(holder: BindingHolder, position: Int) { + val account = getItem(position) + holder.binding.displayNameTextView.text = account.name.emojify(account.emojis, holder.binding.displayNameTextView, animateEmojis) + holder.binding.usernameTextView.text = account.username + loadAvatar(account.avatar, holder.binding.avatar, radius, animateAvatar) } } @@ -232,57 +206,58 @@ class AccountsInListFragment : DialogFragment(), Injectable { return oldItem.second == newItem.second && oldItem.first.deepEquals(newItem.first) } - } - inner class SearchAdapter : ListAdapter(SearchDiffer) { + inner class SearchAdapter : ListAdapter>(SearchDiffer) { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val view = LayoutInflater.from(parent.context) - .inflate(R.layout.item_follow_request, parent, false) - return ViewHolder(view) - } + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder { + val binding = ItemFollowRequestBinding.inflate(LayoutInflater.from(parent.context), parent, false) + val holder = BindingHolder(binding) - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val (account, inAList) = getItem(position) - holder.bind(account, inAList) - - } - - inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), - View.OnClickListener, LayoutContainer { - - override val containerView = itemView - - fun bind(account: Account, inAList: Boolean) { - displayNameTextView.text = account.name.emojify(account.emojis, displayNameTextView, animateEmojis) - usernameTextView.text = account.username - loadAvatar(account.avatar, avatar, radius, animateAvatar) - - rejectButton.apply { - contentDescription = if (inAList) { - setImageResource(R.drawable.ic_reject_24dp) - getString(R.string.action_remove_from_list) - } else { - setImageResource(R.drawable.ic_plus_24dp) - getString(R.string.action_add_to_list) - } - } - } - - init { - acceptButton.hide() - rejectButton.setOnClickListener(this) - } - - override fun onClick(v: View?) { - val (account, inAList) = getItem(adapterPosition) + binding.notificationTextView.hide() + binding.acceptButton.hide() + binding.rejectButton.setOnClickListener { + val (account, inAList) = getItem(holder.bindingAdapterPosition) if (inAList) { onRemoveFromList(account.id) } else { onAddToList(account) } } + + return holder + } + + override fun onBindViewHolder(holder: BindingHolder, position: Int) { + val (account, inAList) = getItem(position) + + holder.binding.displayNameTextView.text = account.name.emojify(account.emojis, holder.binding.displayNameTextView, animateEmojis) + holder.binding.usernameTextView.text = account.username + loadAvatar(account.avatar, holder.binding.avatar, radius, animateAvatar) + + holder.binding.rejectButton.apply { + contentDescription = if (inAList) { + setImageResource(R.drawable.ic_reject_24dp) + getString(R.string.action_remove_from_list) + } else { + setImageResource(R.drawable.ic_plus_24dp) + getString(R.string.action_add_to_list) + } + } + } + } + + companion object { + private const val LIST_ID_ARG = "listId" + private const val LIST_NAME_ARG = "listName" + + @JvmStatic + fun newInstance(listId: String, listName: String): AccountsInListFragment { + val args = Bundle().apply { + putString(LIST_ID_ARG, listId) + putString(LIST_NAME_ARG, listName) + } + return AccountsInListFragment().apply { arguments = args } } } } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt b/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt index 64d952b9..3d7e0380 100644 --- a/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt @@ -38,6 +38,7 @@ import com.bumptech.glide.load.resource.bitmap.FitCenter import com.bumptech.glide.load.resource.bitmap.RoundedCorners import com.google.android.material.snackbar.Snackbar import com.keylesspalace.tusky.adapter.AccountFieldEditAdapter +import com.keylesspalace.tusky.databinding.ActivityEditProfileBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.util.* @@ -47,8 +48,6 @@ import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial import com.mikepenz.iconics.utils.colorInt import com.mikepenz.iconics.utils.sizeDp import com.theartofdev.edmodo.cropper.CropImage -import kotlinx.android.synthetic.main.activity_edit_profile.* -import kotlinx.android.synthetic.main.toolbar_basic.* import javax.inject.Inject class EditProfileActivity : BaseActivity(), Injectable { @@ -71,6 +70,8 @@ class EditProfileActivity : BaseActivity(), Injectable { private val viewModel: EditProfileViewModel by viewModels { viewModelFactory } + private val binding by viewBinding(ActivityEditProfileBinding::inflate) + private var currentlyPicking: PickType = PickType.NOTHING private val accountFieldEditAdapter = AccountFieldEditAdapter() @@ -88,33 +89,33 @@ class EditProfileActivity : BaseActivity(), Injectable { currentlyPicking = PickType.valueOf(it) } - setContentView(R.layout.activity_edit_profile) + setContentView(binding.root) - setSupportActionBar(toolbar) + setSupportActionBar(binding.includedToolbar.toolbar) supportActionBar?.run { setTitle(R.string.title_edit_profile) setDisplayHomeAsUpEnabled(true) setDisplayShowHomeEnabled(true) } - avatarButton.setOnClickListener { onMediaPick(PickType.AVATAR) } - headerButton.setOnClickListener { onMediaPick(PickType.HEADER) } + binding.avatarButton.setOnClickListener { onMediaPick(PickType.AVATAR) } + binding.headerButton.setOnClickListener { onMediaPick(PickType.HEADER) } - fieldList.layoutManager = LinearLayoutManager(this) - fieldList.adapter = accountFieldEditAdapter + binding.fieldList.layoutManager = LinearLayoutManager(this) + binding.fieldList.adapter = accountFieldEditAdapter val plusDrawable = IconicsDrawable(this, GoogleMaterial.Icon.gmd_add).apply { sizeDp = 12; colorInt = Color.WHITE } - addFieldButton.setCompoundDrawablesRelativeWithIntrinsicBounds(plusDrawable, null, null, null) + binding.addFieldButton.setCompoundDrawablesRelativeWithIntrinsicBounds(plusDrawable, null, null, null) - addFieldButton.setOnClickListener { + binding.addFieldButton.setOnClickListener { accountFieldEditAdapter.addField() if(accountFieldEditAdapter.itemCount >= MAX_ACCOUNT_FIELDS) { it.isVisible = false } - scrollView.post{ - scrollView.smoothScrollTo(0, it.bottom) + binding.scrollView.post{ + binding.scrollView.smoothScrollTo(0, it.bottom) } } @@ -126,12 +127,12 @@ class EditProfileActivity : BaseActivity(), Injectable { val me = profileRes.data if (me != null) { - displayNameEditText.setText(me.displayName) - noteEditText.setText(me.source?.note) - lockedCheckBox.isChecked = me.locked + binding.displayNameEditText.setText(me.displayName) + binding.noteEditText.setText(me.source?.note) + binding.lockedCheckBox.isChecked = me.locked accountFieldEditAdapter.setFields(me.source?.fields ?: emptyList()) - addFieldButton.isEnabled = me.source?.fields?.size ?: 0 < MAX_ACCOUNT_FIELDS + binding.addFieldButton.isEnabled = me.source?.fields?.size ?: 0 < MAX_ACCOUNT_FIELDS if(viewModel.avatarData.value == null) { Glide.with(this) @@ -141,19 +142,19 @@ class EditProfileActivity : BaseActivity(), Injectable { FitCenter(), RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp)) ) - .into(avatarPreview) + .into(binding.avatarPreview) } if(viewModel.headerData.value == null) { Glide.with(this) .load(me.header) - .into(headerPreview) + .into(binding.headerPreview) } } } is Error -> { - val snackbar = Snackbar.make(avatarButton, R.string.error_generic, Snackbar.LENGTH_LONG) + val snackbar = Snackbar.make(binding.avatarButton, R.string.error_generic, Snackbar.LENGTH_LONG) snackbar.setAction(R.string.action_retry) { viewModel.obtainProfile() } @@ -169,14 +170,14 @@ class EditProfileActivity : BaseActivity(), Injectable { is Success -> { val instance = result.data if (instance?.maxBioChars != null && instance.maxBioChars > 0) { - noteEditTextLayout.counterMaxLength = instance.maxBioChars + binding.noteEditTextLayout.counterMaxLength = instance.maxBioChars } } } } - observeImage(viewModel.avatarData, avatarPreview, avatarProgressBar, true) - observeImage(viewModel.headerData, headerPreview, headerProgressBar, false) + observeImage(viewModel.avatarData, binding.avatarPreview, binding.avatarProgressBar, true) + observeImage(viewModel.headerData, binding.headerPreview, binding.headerProgressBar, false) viewModel.saveData.observe(this, { when(it) { @@ -184,7 +185,7 @@ class EditProfileActivity : BaseActivity(), Injectable { finish() } is Loading -> { - saveProgressBar.visibility = View.VISIBLE + binding.saveProgressBar.visibility = View.VISIBLE } is Error -> { onSaveFailure(it.errorMessage) @@ -202,9 +203,9 @@ class EditProfileActivity : BaseActivity(), Injectable { override fun onStop() { super.onStop() if(!isFinishing) { - viewModel.updateProfile(displayNameEditText.text.toString(), - noteEditText.text.toString(), - lockedCheckBox.isChecked, + viewModel.updateProfile(binding.displayNameEditText.text.toString(), + binding.noteEditText.text.toString(), + binding.lockedCheckBox.isChecked, accountFieldEditAdapter.getFieldData()) } } @@ -268,7 +269,7 @@ class EditProfileActivity : BaseActivity(), Injectable { initiateMediaPicking() } else { endMediaPicking() - Snackbar.make(avatarButton, R.string.error_media_upload_permission, Snackbar.LENGTH_LONG).show() + Snackbar.make(binding.avatarButton, R.string.error_media_upload_permission, Snackbar.LENGTH_LONG).show() } } } @@ -309,39 +310,38 @@ class EditProfileActivity : BaseActivity(), Injectable { return } - viewModel.save(displayNameEditText.text.toString(), - noteEditText.text.toString(), - lockedCheckBox.isChecked, + viewModel.save(binding.displayNameEditText.text.toString(), + binding.noteEditText.text.toString(), + binding.lockedCheckBox.isChecked, accountFieldEditAdapter.getFieldData(), this) } private fun onSaveFailure(msg: String?) { val errorMsg = msg ?: getString(R.string.error_media_upload_sending) - Snackbar.make(avatarButton, errorMsg, Snackbar.LENGTH_LONG).show() - saveProgressBar.visibility = View.GONE + Snackbar.make(binding.avatarButton, errorMsg, Snackbar.LENGTH_LONG).show() + binding.saveProgressBar.visibility = View.GONE } private fun beginMediaPicking() { when (currentlyPicking) { PickType.AVATAR -> { - avatarProgressBar.visibility = View.VISIBLE - avatarPreview.visibility = View.INVISIBLE - avatarButton.setImageDrawable(null) - + binding.avatarProgressBar.visibility = View.VISIBLE + binding.avatarPreview.visibility = View.INVISIBLE + binding.avatarButton.setImageDrawable(null) } PickType.HEADER -> { - headerProgressBar.visibility = View.VISIBLE - headerPreview.visibility = View.INVISIBLE - headerButton.setImageDrawable(null) + binding.headerProgressBar.visibility = View.VISIBLE + binding.headerPreview.visibility = View.INVISIBLE + binding.headerButton.setImageDrawable(null) } PickType.NOTHING -> { /* do nothing */ } } } private fun endMediaPicking() { - avatarProgressBar.visibility = View.GONE - headerProgressBar.visibility = View.GONE + binding.avatarProgressBar.visibility = View.GONE + binding.headerProgressBar.visibility = View.GONE currentlyPicking = PickType.NOTHING } @@ -402,7 +402,7 @@ class EditProfileActivity : BaseActivity(), Injectable { } private fun onResizeFailure() { - Snackbar.make(avatarButton, R.string.error_media_upload_sending, Snackbar.LENGTH_LONG).show() + Snackbar.make(binding.avatarButton, R.string.error_media_upload_sending, Snackbar.LENGTH_LONG).show() endMediaPicking() } diff --git a/app/src/main/java/com/keylesspalace/tusky/FiltersActivity.kt b/app/src/main/java/com/keylesspalace/tusky/FiltersActivity.kt index 0726b26e..7e91db07 100644 --- a/app/src/main/java/com/keylesspalace/tusky/FiltersActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/FiltersActivity.kt @@ -7,13 +7,13 @@ import android.widget.Toast import androidx.appcompat.app.AlertDialog import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.PreferenceChangedEvent +import com.keylesspalace.tusky.databinding.ActivityFiltersBinding +import com.keylesspalace.tusky.databinding.DialogFilterBinding import com.keylesspalace.tusky.entity.Filter import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.show -import kotlinx.android.synthetic.main.activity_filters.* -import kotlinx.android.synthetic.main.dialog_filter.* -import kotlinx.android.synthetic.main.toolbar_basic.* +import com.keylesspalace.tusky.util.viewBinding import okhttp3.ResponseBody import retrofit2.Call import retrofit2.Callback @@ -28,13 +28,28 @@ class FiltersActivity: BaseActivity() { @Inject lateinit var eventHub: EventHub + private val binding by viewBinding(ActivityFiltersBinding::inflate) + private lateinit var context : String private lateinit var filters: MutableList - private lateinit var dialog: AlertDialog - companion object { - const val FILTERS_CONTEXT = "filters_context" - const val FILTERS_TITLE = "filters_title" + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(binding.root) + setSupportActionBar(binding.includedToolbar.toolbar) + supportActionBar?.run { + // Back button + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + } + binding.addFilterButton.setOnClickListener { + showAddFilterDialog() + } + + title = intent?.getStringExtra(FILTERS_TITLE) + context = intent?.getStringExtra(FILTERS_CONTEXT)!! + loadFilters() } private fun updateFilter(filter: Filter, itemIndex: Int) { @@ -101,52 +116,51 @@ class FiltersActivity: BaseActivity() { } private fun showAddFilterDialog() { - dialog = AlertDialog.Builder(this@FiltersActivity) + val binding = DialogFilterBinding.inflate(layoutInflater) + binding.phraseWholeWord.isChecked = true + AlertDialog.Builder(this@FiltersActivity) .setTitle(R.string.filter_addition_dialog_title) - .setView(R.layout.dialog_filter) + .setView(binding.root) .setPositiveButton(android.R.string.ok){ _, _ -> - createFilter(dialog.phraseEditText.text.toString(), dialog.phraseWholeWord.isChecked) + createFilter(binding.phraseEditText.text.toString(), binding.phraseWholeWord.isChecked) } .setNeutralButton(android.R.string.cancel, null) - .create() - dialog.show() - dialog.phraseWholeWord.isChecked = true + .show() } private fun setupEditDialogForItem(itemIndex: Int) { - dialog = AlertDialog.Builder(this@FiltersActivity) + val binding = DialogFilterBinding.inflate(layoutInflater) + val filter = filters[itemIndex] + binding.phraseEditText.setText(filter.phrase) + binding.phraseWholeWord.isChecked = filter.wholeWord + + AlertDialog.Builder(this@FiltersActivity) .setTitle(R.string.filter_edit_dialog_title) - .setView(R.layout.dialog_filter) + .setView(binding.root) .setPositiveButton(R.string.filter_dialog_update_button) { _, _ -> val oldFilter = filters[itemIndex] - val newFilter = Filter(oldFilter.id, dialog.phraseEditText.text.toString(), oldFilter.context, - oldFilter.expiresAt, oldFilter.irreversible, dialog.phraseWholeWord.isChecked) + val newFilter = Filter(oldFilter.id, binding.phraseEditText.text.toString(), oldFilter.context, + oldFilter.expiresAt, oldFilter.irreversible, binding.phraseWholeWord.isChecked) updateFilter(newFilter, itemIndex) } .setNegativeButton(R.string.filter_dialog_remove_button) { _, _ -> deleteFilter(itemIndex) } .setNeutralButton(android.R.string.cancel, null) - .create() - dialog.show() - - // Need to show the dialog before referencing any elements from its view - val filter = filters[itemIndex] - dialog.phraseEditText.setText(filter.phrase) - dialog.phraseWholeWord.isChecked = filter.wholeWord + .show() } private fun refreshFilterDisplay() { - filtersView.adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, filters.map { filter -> filter.phrase }) - filtersView.onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ -> setupEditDialogForItem(position) } + binding.filtersView.adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, filters.map { filter -> filter.phrase }) + binding.filtersView.onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ -> setupEditDialogForItem(position) } } private fun loadFilters() { - filterMessageView.hide() - filtersView.hide() - addFilterButton.hide() - filterProgressBar.show() + binding.filterMessageView.hide() + binding.filtersView.hide() + binding.addFilterButton.hide() + binding.filterProgressBar.show() api.getFilters().enqueue(object : Callback> { override fun onResponse(call: Call>, response: Response>) { @@ -156,52 +170,33 @@ class FiltersActivity: BaseActivity() { filters = filterResponse.filter { filter -> filter.context.contains(context) }.toMutableList() refreshFilterDisplay() - filtersView.show() - addFilterButton.show() - filterProgressBar.hide() + binding.filtersView.show() + binding.addFilterButton.show() + binding.filterProgressBar.hide() } else { - filterProgressBar.hide() - filterMessageView.show() - filterMessageView.setup(R.drawable.elephant_error, + binding.filterProgressBar.hide() + binding.filterMessageView.show() + binding.filterMessageView.setup(R.drawable.elephant_error, R.string.error_generic) { loadFilters() } } } override fun onFailure(call: Call>, t: Throwable) { - filterProgressBar.hide() - filterMessageView.show() + binding.filterProgressBar.hide() + binding.filterMessageView.show() if (t is IOException) { - filterMessageView.setup(R.drawable.elephant_offline, + binding.filterMessageView.setup(R.drawable.elephant_offline, R.string.error_network) { loadFilters() } } else { - filterMessageView.setup(R.drawable.elephant_error, + binding.filterMessageView.setup(R.drawable.elephant_error, R.string.error_generic) { loadFilters() } } } }) } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - setContentView(R.layout.activity_filters) - setupToolbarBackArrow() - addFilterButton.setOnClickListener { - showAddFilterDialog() - } - - title = intent?.getStringExtra(FILTERS_TITLE) - context = intent?.getStringExtra(FILTERS_CONTEXT)!! - loadFilters() + companion object { + const val FILTERS_CONTEXT = "filters_context" + const val FILTERS_TITLE = "filters_title" } - - private fun setupToolbarBackArrow() { - setSupportActionBar(toolbar) - supportActionBar?.run { - // Back button - setDisplayHomeAsUpEnabled(true) - setDisplayShowHomeEnabled(true) - } - } - } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/LicenseActivity.kt b/app/src/main/java/com/keylesspalace/tusky/LicenseActivity.kt index d6cc7bca..406a4aaf 100644 --- a/app/src/main/java/com/keylesspalace/tusky/LicenseActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/LicenseActivity.kt @@ -19,23 +19,20 @@ import android.os.Bundle import androidx.annotation.RawRes import android.util.Log import android.widget.TextView +import com.keylesspalace.tusky.databinding.ActivityLicenseBinding import com.keylesspalace.tusky.util.IOUtils -import kotlinx.android.extensions.CacheImplementation -import kotlinx.android.extensions.ContainerOptions -import kotlinx.android.synthetic.main.activity_license.* -import kotlinx.android.synthetic.main.toolbar_basic.* import java.io.BufferedReader import java.io.IOException import java.io.InputStreamReader class LicenseActivity : BaseActivity() { - @ContainerOptions(cache = CacheImplementation.NO_CACHE) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_license) + val binding = ActivityLicenseBinding.inflate(layoutInflater) + setContentView(binding.root) - setSupportActionBar(toolbar) + setSupportActionBar(binding.includedToolbar.toolbar) supportActionBar?.run { setDisplayHomeAsUpEnabled(true) setDisplayShowHomeEnabled(true) @@ -43,7 +40,7 @@ class LicenseActivity : BaseActivity() { setTitle(R.string.title_licenses) - loadFileIntoTextView(R.raw.apache, licenseApacheTextView) + loadFileIntoTextView(R.raw.apache, binding.licenseApacheTextView) } @@ -67,7 +64,5 @@ class LicenseActivity : BaseActivity() { IOUtils.closeQuietly(br) textView.text = sb.toString() - } - } diff --git a/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt b/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt index fa3c92c3..be995e9e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt @@ -24,12 +24,14 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.* +import androidx.activity.viewModels import androidx.annotation.StringRes import androidx.appcompat.app.AlertDialog import androidx.recyclerview.widget.* import androidx.recyclerview.widget.ListAdapter import at.connyduck.sparkbutton.helpers.Utils import com.google.android.material.snackbar.Snackbar +import com.keylesspalace.tusky.databinding.ActivityListsBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.entity.MastoList @@ -47,8 +49,6 @@ import com.uber.autodispose.autoDispose import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector import io.reactivex.android.schedulers.AndroidSchedulers -import kotlinx.android.synthetic.main.activity_lists.* -import kotlinx.android.synthetic.main.toolbar_basic.* import javax.inject.Inject /** @@ -57,47 +57,42 @@ import javax.inject.Inject class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector { - companion object { - @JvmStatic - fun newIntent(context: Context): Intent { - return Intent(context, ListsActivity::class.java) - } - } - @Inject lateinit var viewModelFactory: ViewModelFactory @Inject lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector - private lateinit var viewModel: ListsViewModel + private val viewModel: ListsViewModel by viewModels { viewModelFactory } + + private val binding by viewBinding(ActivityListsBinding::inflate) + private val adapter = ListsAdapter() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_lists) + setContentView(binding.root) - setSupportActionBar(toolbar) + setSupportActionBar(binding.includedToolbar.toolbar) supportActionBar?.apply { title = getString(R.string.title_lists) setDisplayHomeAsUpEnabled(true) setDisplayShowHomeEnabled(true) } - listsRecycler.adapter = adapter - listsRecycler.layoutManager = LinearLayoutManager(this) - listsRecycler.addItemDecoration( + binding.listsRecycler.adapter = adapter + binding.listsRecycler.layoutManager = LinearLayoutManager(this) + binding.listsRecycler.addItemDecoration( DividerItemDecoration(this, DividerItemDecoration.VERTICAL)) - viewModel = viewModelFactory.create(ListsViewModel::class.java) viewModel.state .observeOn(AndroidSchedulers.mainThread()) .autoDispose(from(this)) .subscribe(this::update) viewModel.retryLoading() - addListButton.setOnClickListener { + binding.addListButton.setOnClickListener { showlistNameDialog(null) } @@ -153,37 +148,36 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector { private fun update(state: ListsViewModel.State) { adapter.submitList(state.lists) - progressBar.visible(state.loadingState == LOADING) + binding.progressBar.visible(state.loadingState == LOADING) when (state.loadingState) { - INITIAL, LOADING -> messageView.hide() + INITIAL, LOADING -> binding.messageView.hide() ERROR_NETWORK -> { - messageView.show() - messageView.setup(R.drawable.elephant_offline, R.string.error_network) { + binding.messageView.show() + binding.messageView.setup(R.drawable.elephant_offline, R.string.error_network) { viewModel.retryLoading() } } ERROR_OTHER -> { - messageView.show() - messageView.setup(R.drawable.elephant_error, R.string.error_generic) { + binding.messageView.show() + binding.messageView.setup(R.drawable.elephant_error, R.string.error_generic) { viewModel.retryLoading() } } LOADED -> if (state.lists.isEmpty()) { - messageView.show() - messageView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, + binding.messageView.show() + binding.messageView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null) } else { - messageView.hide() + binding.messageView.hide() } } } private fun showMessage(@StringRes messageId: Int) { Snackbar.make( - listsRecycler, messageId, Snackbar.LENGTH_SHORT + binding.listsRecycler, messageId, Snackbar.LENGTH_SHORT ).show() - } private fun onListSelected(listId: String) { @@ -215,8 +209,6 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector { } } - override fun androidInjector() = dispatchingAndroidInjector - private object ListsDiffer : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: MastoList, newItem: MastoList): Boolean { return oldItem.id == newItem.id @@ -258,9 +250,9 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector { override fun onClick(v: View) { if (v == itemView) { - onListSelected(getItem(adapterPosition).id) + onListSelected(getItem(bindingAdapterPosition).id) } else { - onMore(getItem(adapterPosition), v) + onMore(getItem(bindingAdapterPosition), v) } } } @@ -273,4 +265,10 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector { viewModel.renameList(listId, name.toString()) } } + + override fun androidInjector() = dispatchingAndroidInjector + + companion object { + fun newIntent(context: Context) = Intent(context, ListsActivity::class.java) + } } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/LoginActivity.kt b/app/src/main/java/com/keylesspalace/tusky/LoginActivity.kt index 6a6d0647..1acf7dbd 100644 --- a/app/src/main/java/com/keylesspalace/tusky/LoginActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/LoginActivity.kt @@ -29,15 +29,12 @@ import androidx.appcompat.app.AlertDialog import androidx.browser.customtabs.CustomTabColorSchemeParams import androidx.browser.customtabs.CustomTabsIntent import com.bumptech.glide.Glide +import com.keylesspalace.tusky.databinding.ActivityLoginBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.entity.AccessToken import com.keylesspalace.tusky.entity.AppCredentials import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.util.ThemeUtils -import com.keylesspalace.tusky.util.getNonNullString -import com.keylesspalace.tusky.util.rickRoll -import com.keylesspalace.tusky.util.shouldRickRoll -import kotlinx.android.synthetic.main.activity_login.* +import com.keylesspalace.tusky.util.* import okhttp3.HttpUrl import retrofit2.Call import retrofit2.Callback @@ -49,6 +46,8 @@ class LoginActivity : BaseActivity(), Injectable { @Inject lateinit var mastodonApi: MastodonApi + private val binding by viewBinding(ActivityLoginBinding::inflate) + private lateinit var preferences: SharedPreferences private val oauthRedirectUri: String @@ -61,27 +60,27 @@ class LoginActivity : BaseActivity(), Injectable { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_login) + setContentView(binding.root) if(savedInstanceState == null && BuildConfig.CUSTOM_INSTANCE.isNotBlank() && !isAdditionalLogin()) { - domainEditText.setText(BuildConfig.CUSTOM_INSTANCE) - domainEditText.setSelection(BuildConfig.CUSTOM_INSTANCE.length) + binding.domainEditText.setText(BuildConfig.CUSTOM_INSTANCE) + binding.domainEditText.setSelection(BuildConfig.CUSTOM_INSTANCE.length) } if(BuildConfig.CUSTOM_LOGO_URL.isNotBlank()) { - Glide.with(loginLogo) + Glide.with(binding.loginLogo) .load(BuildConfig.CUSTOM_LOGO_URL) .placeholder(null) - .into(loginLogo) + .into(binding.loginLogo) } preferences = getSharedPreferences( getString(R.string.preferences_file_key), Context.MODE_PRIVATE) - loginButton.setOnClickListener { onButtonClick() } - registerButton.setOnClickListener { onRegisterClick() } + binding.loginButton.setOnClickListener { onButtonClick() } + binding.registerButton.setOnClickListener { onRegisterClick() } - whatsAnInstanceTextView.setOnClickListener { + binding.whatsAnInstanceTextView.setOnClickListener { val dialog = AlertDialog.Builder(this) .setMessage(R.string.dialog_whats_an_instance) .setPositiveButton(R.string.action_close, null) @@ -91,11 +90,11 @@ class LoginActivity : BaseActivity(), Injectable { } if (isAdditionalLogin()) { - setSupportActionBar(toolbar) + setSupportActionBar(binding.toolbar) supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayShowTitleEnabled(false) } else { - toolbar.visibility = View.GONE + binding.toolbar.visibility = View.GONE } } @@ -112,7 +111,7 @@ class LoginActivity : BaseActivity(), Injectable { } private fun onRegisterClick() { - registerButton.isEnabled = false + binding.registerButton.isEnabled = false val uri = Uri.parse(BuildConfig.REGISTER_ACCOUNT_URL) if (!openInCustomTab(uri, this)) { @@ -120,7 +119,7 @@ class LoginActivity : BaseActivity(), Injectable { if (viewIntent.resolveActivity(packageManager) != null) { startActivity(viewIntent) } else { - domainEditText.error = getString(R.string.error_no_web_browser_found) + binding.domainEditText.error = getString(R.string.error_no_web_browser_found) setLoading(false) } } @@ -133,15 +132,15 @@ class LoginActivity : BaseActivity(), Injectable { */ private fun onButtonClick() { - loginButton.isEnabled = false + binding.loginButton.isEnabled = false - val domain = canonicalizeDomain(domainEditText.text.toString()) + val domain = canonicalizeDomain(binding.domainEditText.text.toString()) try { HttpUrl.Builder().host(domain).scheme("https").build() } catch (e: IllegalArgumentException) { setLoading(false) - domainTextInputLayout.error = getString(R.string.error_invalid_domain) + binding.domainTextInputLayout.error = getString(R.string.error_invalid_domain) return } @@ -154,8 +153,8 @@ class LoginActivity : BaseActivity(), Injectable { override fun onResponse(call: Call, response: Response) { if (!response.isSuccessful) { - loginButton.isEnabled = true - domainTextInputLayout.error = getString(R.string.error_failed_app_registration) + binding.loginButton.isEnabled = true + binding.domainTextInputLayout.error = getString(R.string.error_failed_app_registration) setLoading(false) Log.e(TAG, "App authentication failed. " + response.message()) return @@ -174,8 +173,8 @@ class LoginActivity : BaseActivity(), Injectable { } override fun onFailure(call: Call, t: Throwable) { - loginButton.isEnabled = true - domainTextInputLayout.error = getString(R.string.error_failed_app_registration) + binding.loginButton.isEnabled = true + binding.domainTextInputLayout.error = getString(R.string.error_failed_app_registration) setLoading(false) Log.e(TAG, Log.getStackTraceString(t)) } @@ -206,7 +205,7 @@ class LoginActivity : BaseActivity(), Injectable { if (viewIntent.resolveActivity(packageManager) != null) { startActivity(viewIntent) } else { - domainEditText.error = getString(R.string.error_no_web_browser_found) + binding.domainEditText.error = getString(R.string.error_no_web_browser_found) setLoading(false) } } @@ -240,7 +239,7 @@ class LoginActivity : BaseActivity(), Injectable { onLoginSuccess(response.body()!!.accessToken, domain) } else { setLoading(false) - domainTextInputLayout.error = getString(R.string.error_retrieving_oauth_token) + binding.domainTextInputLayout.error = getString(R.string.error_retrieving_oauth_token) Log.e(TAG, String.format("%s %s", getString(R.string.error_retrieving_oauth_token), response.message())) @@ -249,7 +248,7 @@ class LoginActivity : BaseActivity(), Injectable { override fun onFailure(call: Call, t: Throwable) { setLoading(false) - domainTextInputLayout.error = getString(R.string.error_retrieving_oauth_token) + binding.domainTextInputLayout.error = getString(R.string.error_retrieving_oauth_token) Log.e(TAG, String.format("%s %s", getString(R.string.error_retrieving_oauth_token), t.message)) @@ -262,14 +261,14 @@ class LoginActivity : BaseActivity(), Injectable { /* Authorization failed. Put the error response where the user can read it and they * can try again. */ setLoading(false) - domainTextInputLayout.error = getString(R.string.error_authorization_denied) + binding.domainTextInputLayout.error = getString(R.string.error_authorization_denied) Log.e(TAG, String.format("%s %s", getString(R.string.error_authorization_denied), error)) } else { // This case means a junk response was received somehow. setLoading(false) - domainTextInputLayout.error = getString(R.string.error_authorization_unknown) + binding.domainTextInputLayout.error = getString(R.string.error_authorization_unknown) } } else { // first show or user cancelled login @@ -279,12 +278,12 @@ class LoginActivity : BaseActivity(), Injectable { private fun setLoading(loadingState: Boolean) { if (loadingState) { - loginLoadingLayout.visibility = View.VISIBLE - loginInputLayout.visibility = View.GONE + binding.loginLoadingLayout.visibility = View.VISIBLE + binding.loginInputLayout.visibility = View.GONE } else { - loginLoadingLayout.visibility = View.GONE - loginInputLayout.visibility = View.VISIBLE - loginButton.isEnabled = true + binding.loginLoadingLayout.visibility = View.GONE + binding.loginInputLayout.visibility = View.VISIBLE + binding.loginButton.isEnabled = true } } diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt index 3b3af8ae..1628d41d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt @@ -59,6 +59,7 @@ import com.keylesspalace.tusky.components.notifications.NotificationHelper import com.keylesspalace.tusky.components.preference.PreferencesActivity import com.keylesspalace.tusky.components.scheduled.ScheduledTootActivity import com.keylesspalace.tusky.components.search.SearchActivity +import com.keylesspalace.tusky.databinding.ActivityMainBinding import com.keylesspalace.tusky.db.AccountEntity import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.entity.Account @@ -86,7 +87,6 @@ import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers -import kotlinx.android.synthetic.main.activity_main.* import javax.inject.Inject class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInjector { @@ -108,6 +108,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje @Inject lateinit var draftHelper: DraftHelper + private val binding by viewBinding(ActivityMainBinding::inflate) + private lateinit var header: AccountHeaderView private var notificationTabPosition = 0 @@ -119,6 +121,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje private lateinit var glide: RequestManager + private var accountLocked: Boolean = false + private val emojiInitCallback = object : InitCallback() { override fun onInitialized() { if (!isDestroyed) { @@ -173,27 +177,27 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje } }) } - } else if (accountRequested) { - // user clicked a notification, show notification tab and switch user if necessary + } else if (accountRequested && savedInstanceState == null) { + // user clicked a notification, show notification tab showNotificationTab = true } } window.statusBarColor = Color.TRANSPARENT // don't draw a status bar, the DrawerLayout and the MaterialDrawerLayout have their own - setContentView(R.layout.activity_main) + setContentView(binding.root) glide = Glide.with(this) - composeButton.setOnClickListener { + binding.composeButton.setOnClickListener { val composeIntent = Intent(applicationContext, ComposeActivity::class.java) startActivity(composeIntent) } val hideTopToolbar = preferences.getBoolean(PrefKeys.HIDE_TOP_TOOLBAR, false) - mainToolbar.visible(!hideTopToolbar) + binding.mainToolbar.visible(!hideTopToolbar) loadDrawerAvatar(activeAccount.profilePictureUrl, true) - mainToolbar.menu.add(R.string.action_search).apply { + binding.mainToolbar.menu.add(R.string.action_search).apply { setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM) icon = IconicsDrawable(this@MainActivity, GoogleMaterial.Icon.gmd_search).apply { sizeDp = 20 @@ -249,11 +253,11 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje override fun onBackPressed() { when { - mainDrawerLayout.isOpen -> { - mainDrawerLayout.close() + binding.mainDrawerLayout.isOpen -> { + binding.mainDrawerLayout.close() } - viewPager.currentItem != 0 -> { - viewPager.currentItem = 0 + binding.viewPager.currentItem != 0 -> { + binding.viewPager.currentItem = 0 } else -> { super.onBackPressed() @@ -264,10 +268,10 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { when (keyCode) { KeyEvent.KEYCODE_MENU -> { - if (mainDrawerLayout.isOpen) { - mainDrawerLayout.close() + if (binding.mainDrawerLayout.isOpen) { + binding.mainDrawerLayout.close() } else { - mainDrawerLayout.open() + binding.mainDrawerLayout.open() } return true } @@ -319,8 +323,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje private fun setupDrawer(savedInstanceState: Bundle?, addSearchButton: Boolean) { - mainToolbar.setNavigationOnClickListener { - mainDrawerLayout.open() + binding.mainToolbar.setNavigationOnClickListener { + binding.mainDrawerLayout.open() } header = AccountHeaderView(this).apply { @@ -333,7 +337,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje descriptionRes = R.string.add_account_description iconicsIcon = GoogleMaterial.Icon.gmd_add }, 0) - attachToSliderView(mainDrawer) + attachToSliderView(binding.mainDrawer) dividerBelowHeader = false closeDrawerOnProfileListClick = true } @@ -369,7 +373,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje } }) - mainDrawer.apply { + binding.mainDrawer.apply { tintStatusBar = true addItems( primaryDrawerItem { @@ -397,6 +401,14 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje startActivityWithSlideInAnimation(intent) } }, + primaryDrawerItem { + nameRes = R.string.action_view_follow_requests + iconicsIcon = GoogleMaterial.Icon.gmd_person_add + onClick = { + val intent = AccountListActivity.newIntent(context, AccountListActivity.Type.FOLLOW_REQUESTS, accountLocked = accountLocked) + startActivityWithSlideInAnimation(intent) + } + }, primaryDrawerItem { nameRes = R.string.action_lists iconicsIcon = GoogleMaterial.Icon.gmd_list @@ -464,7 +476,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje ) if (addSearchButton) { - mainDrawer.addItemsAtPosition(4, + binding.mainDrawer.addItemsAtPosition(4, primaryDrawerItem { nameRes = R.string.action_search iconicsIcon = GoogleMaterial.Icon.gmd_search @@ -478,7 +490,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje } if (BuildConfig.DEBUG) { - mainDrawer.addItems( + binding.mainDrawer.addItems( secondaryDrawerItem { nameText = "debug" isEnabled = false @@ -490,7 +502,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje } override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(mainDrawer.saveInstanceState(outState)) + super.onSaveInstanceState(binding.mainDrawer.saveInstanceState(outState)) } private fun setupTabs(selectNotificationTab: Boolean) { @@ -498,21 +510,21 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje val activeTabLayout = if (preferences.getString("mainNavPosition", "top") == "bottom") { val actionBarSize = ThemeUtils.getDimension(this, R.attr.actionBarSize) val fabMargin = resources.getDimensionPixelSize(R.dimen.fabMargin) - (composeButton.layoutParams as CoordinatorLayout.LayoutParams).bottomMargin = actionBarSize + fabMargin - tabLayout.hide() - bottomTabLayout + (binding.composeButton.layoutParams as CoordinatorLayout.LayoutParams).bottomMargin = actionBarSize + fabMargin + binding.tabLayout.hide() + binding.bottomTabLayout } else { - bottomNav.hide() - (viewPager.layoutParams as CoordinatorLayout.LayoutParams).bottomMargin = 0 - (composeButton.layoutParams as CoordinatorLayout.LayoutParams).anchorId = R.id.viewPager - tabLayout + binding.bottomNav.hide() + (binding.viewPager.layoutParams as CoordinatorLayout.LayoutParams).bottomMargin = 0 + (binding.composeButton.layoutParams as CoordinatorLayout.LayoutParams).anchorId = R.id.viewPager + binding.tabLayout } val tabs = accountManager.activeAccount!!.tabPreferences val adapter = MainPagerAdapter(tabs, this) - viewPager.adapter = adapter - TabLayoutMediator(activeTabLayout, viewPager) { _: TabLayout.Tab?, _: Int -> }.attach() + binding.viewPager.adapter = adapter + TabLayoutMediator(activeTabLayout, binding.viewPager) { _: TabLayout.Tab?, _: Int -> }.attach() activeTabLayout.removeAllTabs() for (i in tabs.indices) { val tab = activeTabLayout.newTab() @@ -533,10 +545,10 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje } val pageMargin = resources.getDimensionPixelSize(R.dimen.tab_page_margin) - viewPager.setPageTransformer(MarginPageTransformer(pageMargin)) + binding.viewPager.setPageTransformer(MarginPageTransformer(pageMargin)) val enableSwipeForTabs = preferences.getBoolean("enableSwipeForTabs", true) - viewPager.isUserInputEnabled = enableSwipeForTabs + binding.viewPager.isUserInputEnabled = enableSwipeForTabs onTabSelectedListener?.let { activeTabLayout.removeOnTabSelectedListener(it) @@ -548,7 +560,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje NotificationHelper.clearNotificationsForActiveAccount(this@MainActivity, accountManager) } - mainToolbar.title = tabs[tab.position].title(this@MainActivity) + binding.mainToolbar.title = tabs[tab.position].title(this@MainActivity) } override fun onTabUnselected(tab: TabLayout.Tab) {} @@ -564,8 +576,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje } val activeTabPosition = if (selectNotificationTab) notificationTabPosition else 0 - mainToolbar.title = tabs[activeTabPosition].title(this@MainActivity) - mainToolbar.setOnClickListener { + binding.mainToolbar.title = tabs[activeTabPosition].title(this@MainActivity) + binding.mainToolbar.setOnClickListener { (adapter.getFragment(activeTabLayout.selectedTabPosition) as? ReselectableFragment)?.onReselect() } @@ -658,22 +670,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje accountManager.updateActiveAccount(me) NotificationHelper.createNotificationChannelsForAccount(accountManager.activeAccount!!, this) - // Show follow requests in the menu, if this is a locked account. - if (me.locked && mainDrawer.getDrawerItem(DRAWER_ITEM_FOLLOW_REQUESTS) == null) { - val followRequestsItem = primaryDrawerItem { - identifier = DRAWER_ITEM_FOLLOW_REQUESTS - nameRes = R.string.action_view_follow_requests - iconicsIcon = GoogleMaterial.Icon.gmd_person_add - onClick = { - val intent = Intent(this@MainActivity, AccountListActivity::class.java) - intent.putExtra("type", AccountListActivity.Type.FOLLOW_REQUESTS) - startActivityWithSlideInAnimation(intent) - } - } - mainDrawer.addItemAtPosition(4, followRequestsItem) - } else if (!me.locked) { - mainDrawer.removeItems(DRAWER_ITEM_FOLLOW_REQUESTS) - } + accountLocked = me.locked + updateProfiles() updateShortcut(this, accountManager.activeAccount!!) } @@ -684,7 +682,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje glide.asDrawable() .load(avatarUrl) .transform( - RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp)) + RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp)) ) .apply { if (showPlaceholder) { @@ -695,16 +693,17 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje override fun onLoadStarted(placeholder: Drawable?) { if (placeholder != null) { - mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize) + binding.mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize) } } + override fun onResourceReady(resource: Drawable, transition: Transition?) { - mainToolbar.navigationIcon = FixedSizeDrawable(resource, navIconSize, navIconSize) + binding.mainToolbar.navigationIcon = FixedSizeDrawable(resource, navIconSize, navIconSize) } override fun onLoadCleared(placeholder: Drawable?) { if (placeholder != null) { - mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize) + binding.mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize) } } }) @@ -726,7 +725,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje } private fun updateAnnouncementsBadge() { - mainDrawer.updateBadge(DRAWER_ITEM_ANNOUNCEMENTS, StringHolder(if (unreadAnnouncementsCount <= 0) null else unreadAnnouncementsCount.toString())) + binding.mainDrawer.updateBadge(DRAWER_ITEM_ANNOUNCEMENTS, StringHolder(if (unreadAnnouncementsCount <= 0) null else unreadAnnouncementsCount.toString())) } private fun updateProfiles() { @@ -779,14 +778,13 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje } - override fun getActionButton(): FloatingActionButton? = composeButton + override fun getActionButton(): FloatingActionButton? = binding.composeButton override fun androidInjector() = androidInjector companion object { private const val TAG = "MainActivity" // logging tag private const val DRAWER_ITEM_ADD_ACCOUNT: Long = -13 - private const val DRAWER_ITEM_FOLLOW_REQUESTS: Long = 10 private const val DRAWER_ITEM_ANNOUNCEMENTS: Long = 14 const val STATUS_URL = "statusUrl" } diff --git a/app/src/main/java/com/keylesspalace/tusky/ModalTimelineActivity.kt b/app/src/main/java/com/keylesspalace/tusky/ModalTimelineActivity.kt index c3017b0c..64c22917 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ModalTimelineActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/ModalTimelineActivity.kt @@ -4,43 +4,28 @@ import android.content.Context import android.content.Intent import android.os.Bundle import com.google.android.material.floatingactionbutton.FloatingActionButton +import com.keylesspalace.tusky.databinding.ActivityModalTimelineBinding import com.keylesspalace.tusky.fragment.TimelineFragment import com.keylesspalace.tusky.interfaces.ActionButtonActivity import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector -import kotlinx.android.synthetic.main.toolbar_basic.* import javax.inject.Inject class ModalTimelineActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInjector { - companion object { - private const val ARG_KIND = "kind" - private const val ARG_ARG = "arg" - - @JvmStatic - fun newIntent(context: Context, kind: TimelineFragment.Kind, - argument: String?): Intent { - val intent = Intent(context, ModalTimelineActivity::class.java) - intent.putExtra(ARG_KIND, kind) - intent.putExtra(ARG_ARG, argument) - return intent - } - - } - @Inject lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_modal_timeline) + val binding = ActivityModalTimelineBinding.inflate(layoutInflater) + setContentView(binding.root) - setSupportActionBar(toolbar) - val bar = supportActionBar - if (bar != null) { - bar.title = getString(R.string.title_list_timeline) - bar.setDisplayHomeAsUpEnabled(true) - bar.setDisplayShowHomeEnabled(true) + setSupportActionBar(binding.includedToolbar.toolbar) + supportActionBar?.apply { + title = getString(R.string.title_list_timeline) + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) } if (supportFragmentManager.findFragmentById(R.id.contentFrame) == null) { @@ -57,4 +42,18 @@ class ModalTimelineActivity : BottomSheetActivity(), ActionButtonActivity, HasAn override fun androidInjector() = dispatchingAndroidInjector + companion object { + private const val ARG_KIND = "kind" + private const val ARG_ARG = "arg" + + @JvmStatic + fun newIntent(context: Context, kind: TimelineFragment.Kind, + argument: String?): Intent { + val intent = Intent(context, ModalTimelineActivity::class.java) + intent.putExtra(ARG_KIND, kind) + intent.putExtra(ARG_ARG, argument) + return intent + } + + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt b/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt index 9eba5bbe..b2691ee9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt @@ -19,6 +19,7 @@ import android.content.Context import android.content.Intent import android.os.Bundle import androidx.fragment.app.commit +import com.keylesspalace.tusky.databinding.ActivityStatuslistBinding import com.keylesspalace.tusky.fragment.TimelineFragment import com.keylesspalace.tusky.fragment.TimelineFragment.Kind @@ -27,9 +28,6 @@ import javax.inject.Inject import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector -import kotlinx.android.extensions.CacheImplementation -import kotlinx.android.extensions.ContainerOptions -import kotlinx.android.synthetic.main.toolbar_basic.* class StatusListActivity : BottomSheetActivity(), HasAndroidInjector { @@ -39,12 +37,12 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector { private val kind: Kind get() = Kind.valueOf(intent.getStringExtra(EXTRA_KIND)!!) - @ContainerOptions(cache = CacheImplementation.NO_CACHE) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_statuslist) + val binding = ActivityStatuslistBinding.inflate(layoutInflater) + setContentView(binding.root) - setSupportActionBar(toolbar) + setSupportActionBar(binding.includedToolbar.toolbar) val title = if(kind == Kind.FAVOURITES) { R.string.title_favourites diff --git a/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt b/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt index 2b61f141..67cd9cb6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt @@ -38,17 +38,17 @@ import com.keylesspalace.tusky.adapter.ListSelectionAdapter import com.keylesspalace.tusky.adapter.TabAdapter import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.MainTabsChangedEvent +import com.keylesspalace.tusky.databinding.ActivityTabPreferenceBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.onTextChanged +import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.visible import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from import com.uber.autodispose.autoDispose import io.reactivex.Single import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers -import kotlinx.android.synthetic.main.activity_tab_preference.* -import kotlinx.android.synthetic.main.toolbar_basic.* import java.util.regex.Pattern import javax.inject.Inject @@ -59,6 +59,8 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene @Inject lateinit var eventHub: EventHub + private val binding by viewBinding(ActivityTabPreferenceBinding::inflate) + private lateinit var currentTabs: MutableList private lateinit var currentTabsAdapter: TabAdapter private lateinit var touchHelper: ItemTouchHelper @@ -73,9 +75,9 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_tab_preference) + setContentView(binding.root) - setSupportActionBar(toolbar) + setSupportActionBar(binding.includedToolbar.toolbar) supportActionBar?.apply { setTitle(R.string.title_tab_preferences) @@ -85,13 +87,13 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene currentTabs = accountManager.activeAccount?.tabPreferences.orEmpty().toMutableList() currentTabsAdapter = TabAdapter(currentTabs, false, this, currentTabs.size <= MIN_TAB_COUNT) - currentTabsRecyclerView.adapter = currentTabsAdapter - currentTabsRecyclerView.layoutManager = LinearLayoutManager(this) - currentTabsRecyclerView.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL)) + binding.currentTabsRecyclerView.adapter = currentTabsAdapter + binding.currentTabsRecyclerView.layoutManager = LinearLayoutManager(this) + binding.currentTabsRecyclerView.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL)) addTabAdapter = TabAdapter(listOf(createTabDataFromId(DIRECT)), true, this) - addTabRecyclerView.adapter = addTabAdapter - addTabRecyclerView.layoutManager = LinearLayoutManager(this) + binding.addTabRecyclerView.adapter = addTabAdapter + binding.addTabRecyclerView.layoutManager = LinearLayoutManager(this) touchHelper = ItemTouchHelper(object : ItemTouchHelper.Callback() { override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int { @@ -107,17 +109,17 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene } override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean { - val temp = currentTabs[viewHolder.adapterPosition] - currentTabs[viewHolder.adapterPosition] = currentTabs[target.adapterPosition] - currentTabs[target.adapterPosition] = temp + val temp = currentTabs[viewHolder.bindingAdapterPosition] + currentTabs[viewHolder.bindingAdapterPosition] = currentTabs[target.bindingAdapterPosition] + currentTabs[target.bindingAdapterPosition] = temp - currentTabsAdapter.notifyItemMoved(viewHolder.adapterPosition, target.adapterPosition) + currentTabsAdapter.notifyItemMoved(viewHolder.bindingAdapterPosition, target.bindingAdapterPosition) saveTabs() return true } override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { - onTabRemoved(viewHolder.adapterPosition) + onTabRemoved(viewHolder.bindingAdapterPosition) } override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { @@ -132,17 +134,17 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene } }) - touchHelper.attachToRecyclerView(currentTabsRecyclerView) + touchHelper.attachToRecyclerView(binding.currentTabsRecyclerView) - actionButton.setOnClickListener { + binding.actionButton.setOnClickListener { toggleFab(true) } - scrim.setOnClickListener { + binding.scrim.setOnClickListener { toggleFab(false) } - maxTabsInfo.text = getString(R.string.max_tab_number_reached, MAX_TAB_COUNT) + binding.maxTabsInfo.text = resources.getQuantityString(R.plurals.max_tab_number_reached, MAX_TAB_COUNT, MAX_TAB_COUNT) updateAvailableTabs() } @@ -193,18 +195,18 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene private fun toggleFab(expand: Boolean) { val transition = MaterialContainerTransform().apply { - startView = if (expand) actionButton else sheet - val endView: View = if (expand) sheet else actionButton + startView = if (expand) binding.actionButton else binding.sheet + val endView: View = if (expand) binding.sheet else binding.actionButton this.endView = endView addTarget(endView) scrimColor = Color.TRANSPARENT setPathMotion(MaterialArcMotion()) } - TransitionManager.beginDelayedTransition(tabPreferenceContainer, transition) - actionButton.visible(!expand) - sheet.visible(expand) - scrim.visible(expand) + TransitionManager.beginDelayedTransition(binding.root, transition) + binding.actionButton.visible(!expand) + binding.sheet.visible(expand) + binding.scrim.visible(expand) } private fun showAddHashtagDialog(tab: TabData? = null, tabPosition: Int = 0) { @@ -310,7 +312,7 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene addTabAdapter.updateData(addableTabs) - maxTabsInfo.visible(addableTabs.size == 0 || currentTabs.size >= MAX_TAB_COUNT) + binding.maxTabsInfo.visible(addableTabs.size == 0 || currentTabs.size >= MAX_TAB_COUNT) currentTabsAdapter.setRemoveButtonVisible(currentTabs.size > MIN_TAB_COUNT) } @@ -337,7 +339,7 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene } override fun onBackPressed() { - if (actionButton.isVisible) { + if (binding.actionButton.isVisible) { super.onBackPressed() } else { toggleFab(false) diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt b/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt index 5d90bd94..86205b29 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt @@ -44,18 +44,19 @@ import androidx.viewpager2.widget.ViewPager2 import com.bumptech.glide.Glide import com.bumptech.glide.request.FutureTarget import com.keylesspalace.tusky.BuildConfig.APPLICATION_ID +import com.keylesspalace.tusky.databinding.ActivityViewMediaBinding import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.fragment.ViewImageFragment import com.keylesspalace.tusky.pager.SingleImagePagerAdapter import com.keylesspalace.tusky.pager.ImagePagerAdapter import com.keylesspalace.tusky.util.getTemporaryMediaFilename +import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.viewdata.AttachmentViewData import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider import com.uber.autodispose.autoDispose import io.reactivex.Single import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers -import kotlinx.android.synthetic.main.activity_view_media.* import java.io.File import java.io.FileNotFoundException import java.io.FileOutputStream @@ -65,27 +66,11 @@ import java.util.* typealias ToolbarVisibilityListener = (isVisible: Boolean) -> Unit class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener { - companion object { - private const val EXTRA_ATTACHMENTS = "attachments" - private const val EXTRA_ATTACHMENT_INDEX = "index" - private const val EXTRA_SINGLE_IMAGE_URL = "single_image" - private const val TAG = "ViewMediaActivity" - @JvmStatic - fun newIntent(context: Context?, attachments: List, index: Int): Intent { - val intent = Intent(context, ViewMediaActivity::class.java) - intent.putParcelableArrayListExtra(EXTRA_ATTACHMENTS, ArrayList(attachments)) - intent.putExtra(EXTRA_ATTACHMENT_INDEX, index) - return intent - } + private val binding by viewBinding(ActivityViewMediaBinding::inflate) - @JvmStatic - fun newSingleImageIntent(context: Context, url: String): Intent { - val intent = Intent(context, ViewMediaActivity::class.java) - intent.putExtra(EXTRA_SINGLE_IMAGE_URL, url) - return intent - } - } + val toolbar: View + get() = binding.toolbar var isToolbarVisible = true private set @@ -102,7 +87,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_view_media) + setContentView(binding.root) supportPostponeEnterTransition() @@ -125,24 +110,24 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener SingleImagePagerAdapter(this, imageUrl!!) } - viewPager.adapter = adapter - viewPager.setCurrentItem(initialPosition, false) - viewPager.registerOnPageChangeCallback(object: ViewPager2.OnPageChangeCallback() { + binding.viewPager.adapter = adapter + binding.viewPager.setCurrentItem(initialPosition, false) + binding.viewPager.registerOnPageChangeCallback(object: ViewPager2.OnPageChangeCallback() { override fun onPageSelected(position: Int) { - toolbar.title = getPageTitle(position) + binding.toolbar.title = getPageTitle(position) } }) // Setup the toolbar. - setSupportActionBar(toolbar) + setSupportActionBar(binding.toolbar) val actionBar = supportActionBar if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true) actionBar.setDisplayShowHomeEnabled(true) actionBar.title = getPageTitle(initialPosition) } - toolbar.setNavigationOnClickListener { supportFinishAfterTransition() } - toolbar.setOnMenuItemClickListener { item: MenuItem -> + binding.toolbar.setNavigationOnClickListener { supportFinishAfterTransition() } + binding.toolbar.setOnMenuItemClickListener { item: MenuItem -> when (item.itemId) { R.id.action_download -> requestDownloadMedia() R.id.action_open_status -> onOpenStatus() @@ -156,7 +141,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener window.statusBarColor = Color.BLACK window.sharedElementEnterTransition.addListener(object : NoopTransitionListener { override fun onTransitionEnd(transition: Transition) { - adapter.onTransitionEnd(viewPager.currentItem) + adapter.onTransitionEnd(binding.viewPager.currentItem) window.sharedElementEnterTransition.removeListener(this) } }) @@ -165,7 +150,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.view_media_toolbar, menu) // We don't support 'open status' from single image views - menu?.findItem(R.id.action_open_status)?.isVisible = (attachments != null) + menu.findItem(R.id.action_open_status)?.isVisible = (attachments != null) return true } @@ -192,14 +177,14 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener val alpha = if (isToolbarVisible) 1.0f else 0.0f if (isToolbarVisible) { // If to be visible, need to make visible immediately and animate alpha - toolbar.alpha = 0.0f - toolbar.visibility = visibility + binding.toolbar.alpha = 0.0f + binding.toolbar.visibility = visibility } - toolbar.animate().alpha(alpha) + binding.toolbar.animate().alpha(alpha) .setListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { - toolbar.visibility = visibility + binding.toolbar.visibility = visibility animation.removeListener(this) } }) @@ -214,7 +199,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener } private fun downloadMedia() { - val url = imageUrl ?: attachments!![viewPager.currentItem].attachment.url + val url = imageUrl ?: attachments!![binding.viewPager.currentItem].attachment.url val filename = Uri.parse(url).lastPathSegment Toast.makeText(applicationContext, resources.getString(R.string.download_image, filename), Toast.LENGTH_SHORT).show() @@ -230,18 +215,18 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { downloadMedia() } else { - showErrorDialog(toolbar, R.string.error_media_download_permission, R.string.action_retry) { requestDownloadMedia() } + showErrorDialog(binding.toolbar, R.string.error_media_download_permission, R.string.action_retry) { requestDownloadMedia() } } } } private fun onOpenStatus() { - val attach = attachments!![viewPager.currentItem] + val attach = attachments!![binding.viewPager.currentItem] startActivityWithSlideInAnimation(ViewThreadActivity.startIntent(this, attach.statusId, attach.statusUrl)) } private fun copyLink() { - val url = imageUrl ?: attachments!![viewPager.currentItem].attachment.url + val url = imageUrl ?: attachments!![binding.viewPager.currentItem].attachment.url val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager clipboard.setPrimaryClip(ClipData.newPlainText(null, url)) } @@ -256,7 +241,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener if (imageUrl != null) { shareImage(directory, imageUrl!!) } else { - val attachment = attachments!![viewPager.currentItem].attachment + val attachment = attachments!![binding.viewPager.currentItem].attachment when (attachment.type) { Attachment.Type.IMAGE -> shareImage(directory, attachment.url) Attachment.Type.AUDIO, @@ -280,7 +265,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener private fun shareImage(directory: File, url: String) { isCreating = true - progressBarShare.visibility = View.VISIBLE + binding.progressBarShare.visibility = View.VISIBLE invalidateOptionsMenu() val file = File(directory, getTemporaryMediaFilename("png")) val futureTask: FutureTarget = @@ -312,14 +297,14 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener Log.d(TAG, "Download image result: $result") isCreating = false invalidateOptionsMenu() - progressBarShare.visibility = View.GONE + binding.progressBarShare.visibility = View.GONE if (result) shareFile(file, "image/png") }, { error -> isCreating = false invalidateOptionsMenu() - progressBarShare.visibility = View.GONE + binding.progressBarShare.visibility = View.GONE Log.e(TAG, "Failed to download image", error) } ) @@ -342,6 +327,28 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener shareFile(file, mimeType) } + + companion object { + private const val EXTRA_ATTACHMENTS = "attachments" + private const val EXTRA_ATTACHMENT_INDEX = "index" + private const val EXTRA_SINGLE_IMAGE_URL = "single_image" + private const val TAG = "ViewMediaActivity" + + @JvmStatic + fun newIntent(context: Context?, attachments: List, index: Int): Intent { + val intent = Intent(context, ViewMediaActivity::class.java) + intent.putParcelableArrayListExtra(EXTRA_ATTACHMENTS, ArrayList(attachments)) + intent.putExtra(EXTRA_ATTACHMENT_INDEX, index) + return intent + } + + @JvmStatic + fun newSingleImageIntent(context: Context, url: String): Intent { + val intent = Intent(context, ViewMediaActivity::class.java) + intent.putExtra(EXTRA_SINGLE_IMAGE_URL, url) + return intent + } + } } abstract class ViewMediaAdapter(activity: FragmentActivity): FragmentStateAdapter(activity) { diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldAdapter.kt index e395a7e6..fe3b15f8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldAdapter.kt @@ -19,60 +19,57 @@ import android.text.method.LinkMovementMethod import androidx.recyclerview.widget.RecyclerView import android.view.LayoutInflater import android.view.ViewGroup -import android.view.View -import android.widget.TextView import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ItemAccountFieldBinding import com.keylesspalace.tusky.entity.Emoji import com.keylesspalace.tusky.entity.Field import com.keylesspalace.tusky.entity.IdentityProof import com.keylesspalace.tusky.interfaces.LinkListener import com.keylesspalace.tusky.util.* -import kotlinx.android.synthetic.main.item_account_field.view.* -class AccountFieldAdapter(private val linkListener: LinkListener, private val animateEmojis: Boolean) : RecyclerView.Adapter() { +class AccountFieldAdapter( + private val linkListener: LinkListener, + private val animateEmojis: Boolean +) : RecyclerView.Adapter>() { var emojis: List = emptyList() var fields: List> = emptyList() override fun getItemCount() = fields.size - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val view = LayoutInflater.from(parent.context).inflate(R.layout.item_account_field, parent, false) - return ViewHolder(view) + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder { + val binding = ItemAccountFieldBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return BindingHolder(binding) } - override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { + override fun onBindViewHolder(holder: BindingHolder, position: Int) { val proofOrField = fields[position] + val nameTextView = holder.binding.accountFieldName + val valueTextView = holder.binding.accountFieldValue if(proofOrField.isLeft()) { val identityProof = proofOrField.asLeft() - viewHolder.nameTextView.text = identityProof.provider - viewHolder.valueTextView.text = LinkHelper.createClickableText(identityProof.username, identityProof.profileUrl) + nameTextView.text = identityProof.provider + valueTextView.text = LinkHelper.createClickableText(identityProof.username, identityProof.profileUrl) - viewHolder.valueTextView.movementMethod = LinkMovementMethod.getInstance() + valueTextView.movementMethod = LinkMovementMethod.getInstance() - viewHolder.valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0) + valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0) } else { val field = proofOrField.asRight() - val emojifiedName = field.name.emojify(emojis, viewHolder.nameTextView, animateEmojis) - viewHolder.nameTextView.text = emojifiedName + val emojifiedName = field.name.emojify(emojis, nameTextView, animateEmojis) + nameTextView.text = emojifiedName - val emojifiedValue = field.value.emojify(emojis, viewHolder.valueTextView, animateEmojis) - LinkHelper.setClickableText(viewHolder.valueTextView, emojifiedValue, null, linkListener) + val emojifiedValue = field.value.emojify(emojis, valueTextView, animateEmojis) + LinkHelper.setClickableText(valueTextView, emojifiedValue, null, linkListener) if(field.verifiedAt != null) { - viewHolder.valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0) + valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0) } else { - viewHolder.valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0 ) + valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0 ) } } } - - class ViewHolder(rootView: View) : RecyclerView.ViewHolder(rootView) { - val nameTextView: TextView = rootView.accountFieldName - val valueTextView: TextView = rootView.accountFieldValue - } - } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldEditAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldEditAdapter.kt index 768c2885..f7f4553a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldEditAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldEditAdapter.kt @@ -15,18 +15,16 @@ package com.keylesspalace.tusky.adapter -import androidx.recyclerview.widget.RecyclerView import android.text.Editable import android.text.TextWatcher import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup -import android.widget.EditText -import com.keylesspalace.tusky.R +import androidx.recyclerview.widget.RecyclerView +import com.keylesspalace.tusky.databinding.ItemEditFieldBinding import com.keylesspalace.tusky.entity.StringField -import kotlinx.android.synthetic.main.item_edit_field.view.* +import com.keylesspalace.tusky.util.BindingHolder -class AccountFieldEditAdapter : RecyclerView.Adapter() { +class AccountFieldEditAdapter : RecyclerView.Adapter>() { private val fieldData = mutableListOf() @@ -54,20 +52,20 @@ class AccountFieldEditAdapter : RecyclerView.Adapter { + val binding = ItemEditFieldBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return BindingHolder(binding) } - override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { - viewHolder.nameTextView.setText(fieldData[position].first) - viewHolder.valueTextView.setText(fieldData[position].second) + override fun onBindViewHolder(holder: BindingHolder, position: Int) { + holder.binding.accountFieldName.setText(fieldData[position].first) + holder.binding.accountFieldValue.setText(fieldData[position].second) - viewHolder.nameTextView.addTextChangedListener(object: TextWatcher { + holder.binding.accountFieldName.addTextChangedListener(object: TextWatcher { override fun afterTextChanged(newText: Editable) { - fieldData[viewHolder.adapterPosition].first = newText.toString() + fieldData[holder.bindingAdapterPosition].first = newText.toString() } override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} @@ -75,9 +73,9 @@ class AccountFieldEditAdapter : RecyclerView.Adapter(context, R.layout.item_autocomplete_account) { - override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { - var view = convertView - if (convertView == null) { - val layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater - view = layoutInflater.inflate(R.layout.item_autocomplete_account, parent, false) + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + val binding = if (convertView == null) { + ItemAutocompleteAccountBinding.inflate(LayoutInflater.from(context), parent, false) + } else { + ItemAutocompleteAccountBinding.bind(convertView) } - view!! val account = getItem(position) if (account != null) { - val username = view.username - val displayName = view.display_name - val avatar = view.avatar - val pm = PreferenceManager.getDefaultSharedPreferences(avatar.context) + val pm = PreferenceManager.getDefaultSharedPreferences(binding.avatar.context) val animateEmojis = pm.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) - username.text = account.fullName - displayName.text = account.displayName.emojify(account.emojis, displayName, animateEmojis) + binding.username.text = account.fullName + binding.displayName.text = account.displayName.emojify(account.emojis, binding.displayName, animateEmojis) - val avatarRadius = avatar.context.resources.getDimensionPixelSize(R.dimen.avatar_radius_42dp) + val avatarRadius = context.resources.getDimensionPixelSize(R.dimen.avatar_radius_42dp) val animateAvatar = pm.getBoolean("animateGifAvatars", false) - loadAvatar(account.profilePictureUrl, avatar, avatarRadius, animateAvatar) + loadAvatar(account.profilePictureUrl, binding.avatar, avatarRadius, animateAvatar) } - return view + return binding.root } } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/BlocksAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/BlocksAdapter.java index 13144cb8..57cc9035 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/BlocksAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/BlocksAdapter.java @@ -95,7 +95,7 @@ public class BlocksAdapter extends AccountAdapter { void setupActionListener(final AccountActionListener listener) { unblock.setOnClickListener(v -> { - int position = getAdapterPosition(); + int position = getBindingAdapterPosition(); if (position != RecyclerView.NO_POSITION) { listener.onBlock(false, id, position); } 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 70a6163d..2640caac 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt @@ -15,48 +15,44 @@ package com.keylesspalace.tusky.adapter -import androidx.recyclerview.widget.RecyclerView import android.view.LayoutInflater import android.view.ViewGroup -import android.widget.ImageView +import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide -import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ItemEmojiButtonBinding import com.keylesspalace.tusky.entity.Emoji +import com.keylesspalace.tusky.util.BindingHolder import java.util.* -class EmojiAdapter(emojiList: List, private val onEmojiSelectedListener: OnEmojiSelectedListener) : RecyclerView.Adapter() { - private val emojiList : List +class EmojiAdapter( + emojiList: List, + private val onEmojiSelectedListener: OnEmojiSelectedListener +) : RecyclerView.Adapter>() { - init { - this.emojiList = emojiList.filter { emoji -> emoji.visibleInPicker == null || emoji.visibleInPicker } - .sortedBy { it.shortcode.toLowerCase(Locale.ROOT) } + private val emojiList : List = emojiList.filter { emoji -> emoji.visibleInPicker == null || emoji.visibleInPicker } + .sortedBy { it.shortcode.toLowerCase(Locale.ROOT) } + + override fun getItemCount() = emojiList.size + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder { + val binding = ItemEmojiButtonBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return BindingHolder(binding) } - override fun getItemCount(): Int { - return emojiList.size - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EmojiHolder { - val view = LayoutInflater.from(parent.context).inflate(R.layout.item_emoji_button, parent, false) as ImageView - return EmojiHolder(view) - } - - override fun onBindViewHolder(viewHolder: EmojiHolder, position: Int) { + override fun onBindViewHolder(holder: BindingHolder, position: Int) { val emoji = emojiList[position] + val emojiImageView = holder.binding.root - Glide.with(viewHolder.emojiImageView) + Glide.with(emojiImageView) .load(emoji.url) - .into(viewHolder.emojiImageView) + .into(emojiImageView) - viewHolder.emojiImageView.setOnClickListener { + emojiImageView.setOnClickListener { onEmojiSelectedListener.onEmojiSelected(emoji.shortcode) } - viewHolder.emojiImageView.contentDescription = emoji.shortcode + emojiImageView.contentDescription = emoji.shortcode } - - class EmojiHolder(val emojiImageView: ImageView) : RecyclerView.ViewHolder(emojiImageView) - } interface OnEmojiSelectedListener { diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt index 8fa14731..a7e92743 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt @@ -1,55 +1,67 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + package com.keylesspalace.tusky.adapter import android.graphics.Typeface import android.text.SpannableStringBuilder import android.text.Spanned import android.text.style.StyleSpan -import android.view.View -import androidx.preference.PreferenceManager import androidx.recyclerview.widget.RecyclerView import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.interfaces.AccountActionListener import com.keylesspalace.tusky.util.* -import kotlinx.android.synthetic.main.item_follow_request_notification.view.* -internal class FollowRequestViewHolder( - itemView: View, - private val showHeader: Boolean) : RecyclerView.ViewHolder(itemView) { - private var id: String? = null +class FollowRequestViewHolder( + private val binding: ItemFollowRequestBinding, + private val showHeader: Boolean +) : RecyclerView.ViewHolder(binding.root) { fun setupWithAccount(account: Account, animateAvatar: Boolean, animateEmojis: Boolean) { - id = account.id val wrappedName = account.name.unicodeWrap() val emojifiedName: CharSequence = wrappedName.emojify(account.emojis, itemView, animateEmojis) - itemView.displayNameTextView.text = emojifiedName + binding.displayNameTextView.text = emojifiedName if (showHeader) { val wholeMessage: String = itemView.context.getString(R.string.notification_follow_request_format, wrappedName) - itemView.notificationTextView?.text = SpannableStringBuilder(wholeMessage).apply { + binding.notificationTextView.text = SpannableStringBuilder(wholeMessage).apply { setSpan(StyleSpan(Typeface.BOLD), 0, wrappedName.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) }.emojify(account.emojis, itemView, animateEmojis) } - itemView.notificationTextView?.visible(showHeader) + binding.notificationTextView.visible(showHeader) val format = itemView.context.getString(R.string.status_username_format) val formattedUsername = String.format(format, account.username) - itemView.usernameTextView.text = formattedUsername - val avatarRadius = itemView.avatar.context.resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp) - loadAvatar(account.avatar, itemView.avatar, avatarRadius, animateAvatar) + binding.usernameTextView.text = formattedUsername + val avatarRadius = binding.avatar.context.resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp) + loadAvatar(account.avatar, binding.avatar, avatarRadius, animateAvatar) } - fun setupActionListener(listener: AccountActionListener) { - itemView.acceptButton.setOnClickListener { - val position = adapterPosition + fun setupActionListener(listener: AccountActionListener, accountId: String) { + binding.acceptButton.setOnClickListener { + val position = bindingAdapterPosition if (position != RecyclerView.NO_POSITION) { - listener.onRespondToFollowRequest(true, id, position) + listener.onRespondToFollowRequest(true, accountId, position) } } - itemView.rejectButton.setOnClickListener { - val position = adapterPosition + binding.rejectButton.setOnClickListener { + val position = bindingAdapterPosition if (position != RecyclerView.NO_POSITION) { - listener.onRespondToFollowRequest(false, id, position) + listener.onRespondToFollowRequest(false, accountId, position) } } - itemView.setOnClickListener { listener.onViewAccount(id) } + itemView.setOnClickListener { listener.onViewAccount(accountId) } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java index 9ba59884..ef14618e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java @@ -23,6 +23,7 @@ import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding; import com.keylesspalace.tusky.interfaces.AccountActionListener; public class FollowRequestsAdapter extends AccountAdapter { @@ -37,9 +38,8 @@ public class FollowRequestsAdapter extends AccountAdapter { switch (viewType) { default: case VIEW_TYPE_ACCOUNT: { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.item_follow_request, parent, false); - return new FollowRequestViewHolder(view, false); + ItemFollowRequestBinding binding = ItemFollowRequestBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new FollowRequestViewHolder(binding, false); } case VIEW_TYPE_FOOTER: { View view = LayoutInflater.from(parent.getContext()) @@ -54,7 +54,7 @@ public class FollowRequestsAdapter extends AccountAdapter { if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) { FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder; holder.setupWithAccount(accountList.get(position), animateAvatar, animateEmojis); - holder.setupActionListener(accountActionListener); + holder.setupActionListener(accountActionListener, accountList.get(position).getId()); } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsHeaderAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsHeaderAdapter.kt new file mode 100644 index 00000000..60ab4008 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsHeaderAdapter.kt @@ -0,0 +1,40 @@ +/* Copyright 2020 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.keylesspalace.tusky.R + +class FollowRequestsHeaderAdapter(private val instanceName: String, private val accountLocked: Boolean) : RecyclerView.Adapter() { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HeaderViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_follow_requests_header, parent, false) as TextView + return HeaderViewHolder(view) + } + + override fun onBindViewHolder(viewHolder: HeaderViewHolder, position: Int) { + viewHolder.textView.text = viewHolder.textView.context.getString(R.string.follow_requests_info, instanceName) + } + + override fun getItemCount() = if (accountLocked) 0 else 1 + +} + +class HeaderViewHolder(var textView: TextView) : RecyclerView.ViewHolder(textView) diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/HashtagViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/HashtagViewHolder.kt deleted file mode 100644 index c70076c8..00000000 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/HashtagViewHolder.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.keylesspalace.tusky.adapter - -import android.view.View -import android.widget.TextView -import androidx.recyclerview.widget.RecyclerView -import com.keylesspalace.tusky.R -import com.keylesspalace.tusky.interfaces.LinkListener - -class HashtagViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - private val hashtag: TextView = itemView.findViewById(R.id.hashtag) - - fun setup(tag: String, listener: LinkListener) { - hashtag.text = String.format("#%s", tag) - hashtag.setOnClickListener { listener.onViewTag(tag) } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/ListSelectionAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/ListSelectionAdapter.kt index f9b19c69..d6478427 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/ListSelectionAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/ListSelectionAdapter.kt @@ -21,21 +21,22 @@ import android.view.View import android.view.ViewGroup import android.widget.ArrayAdapter import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ItemPickerListBinding import com.keylesspalace.tusky.entity.MastoList -import kotlinx.android.synthetic.main.item_picker_list.view.* -class ListSelectionAdapter(context: Context) : ArrayAdapter(context, R.layout.item_autocomplete_hashtag) { +class ListSelectionAdapter(context: Context) : ArrayAdapter(context, R.layout.item_picker_list) { override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { - val layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater - - val view = convertView - ?: layoutInflater.inflate(R.layout.item_picker_list, parent, false) - - getItem(position)?.let { list -> - view.title.text = list.title + val binding = if (convertView == null) { + ItemPickerListBinding.inflate(LayoutInflater.from(context), parent, false) + } else { + ItemPickerListBinding.bind(convertView) } - return view + getItem(position)?.let { list -> + binding.root.text = list.title + } + + return binding.root } } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.java index e1a30759..f63af6ca 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.java @@ -9,7 +9,6 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.core.view.ViewCompat; -import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.RecyclerView; import com.keylesspalace.tusky.R; @@ -123,9 +122,9 @@ public class MutesAdapter extends AccountAdapter { } void setupActionListener(final AccountActionListener listener) { - unmute.setOnClickListener(v -> listener.onMute(false, id, getAdapterPosition(), false)); + unmute.setOnClickListener(v -> listener.onMute(false, id, getBindingAdapterPosition(), false)); muteNotifications.setOnClickListener( - v -> listener.onMute(true, id, getAdapterPosition(), !notifications)); + v -> listener.onMute(true, id, getBindingAdapterPosition(), !notifications)); itemView.setOnClickListener(v -> listener.onViewAccount(id)); } } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/NetworkStateViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/NetworkStateViewHolder.kt index 66065c7a..b45ca95f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/NetworkStateViewHolder.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/NetworkStateViewHolder.kt @@ -16,29 +16,28 @@ package com.keylesspalace.tusky.adapter import androidx.recyclerview.widget.RecyclerView -import android.view.View import android.view.ViewGroup +import com.keylesspalace.tusky.databinding.ItemNetworkStateBinding import com.keylesspalace.tusky.util.NetworkState import com.keylesspalace.tusky.util.Status import com.keylesspalace.tusky.util.visible -import kotlinx.android.synthetic.main.item_network_state.view.* -class NetworkStateViewHolder(itemView: View, +class NetworkStateViewHolder(private val binding: ItemNetworkStateBinding, private val retryCallback: () -> Unit) -: RecyclerView.ViewHolder(itemView) { +: RecyclerView.ViewHolder(binding.root) { fun setUpWithNetworkState(state: NetworkState?, fullScreen: Boolean) { - itemView.progressBar.visible(state?.status == Status.RUNNING) - itemView.retryButton.visible(state?.status == Status.FAILED) - itemView.errorMsg.visible(state?.msg != null) - itemView.errorMsg.text = state?.msg - itemView.retryButton.setOnClickListener { + binding.progressBar.visible(state?.status == Status.RUNNING) + binding.retryButton.visible(state?.status == Status.FAILED) + binding.errorMsg.visible(state?.msg != null) + binding.errorMsg.text = state?.msg + binding.retryButton.setOnClickListener { retryCallback() } if(fullScreen) { - itemView.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT + binding.root.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT } else { - itemView.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT + binding.root.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT } } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java index 750d40d6..9f212ccc 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java @@ -39,6 +39,7 @@ import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding; import com.keylesspalace.tusky.entity.Account; import com.keylesspalace.tusky.entity.Emoji; import com.keylesspalace.tusky.entity.Notification; @@ -125,9 +126,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter { return new FollowViewHolder(view, statusDisplayOptions); } case VIEW_TYPE_FOLLOW_REQUEST: { - View view = inflater - .inflate(R.layout.item_follow_request_notification, parent, false); - return new FollowRequestViewHolder(view, true); + ItemFollowRequestBinding binding = ItemFollowRequestBinding.inflate(inflater, parent, false); + return new FollowRequestViewHolder(binding, true); } case VIEW_TYPE_PLACEHOLDER: { View view = inflater @@ -233,8 +233,9 @@ public class NotificationsAdapter extends RecyclerView.Adapter { if (payloadForHolder == null) { FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder; holder.setupWithAccount(concreteNotificaton.getAccount(), statusDisplayOptions.animateAvatars(), statusDisplayOptions.animateEmojis()); - holder.setupActionListener(accountActionListener); + holder.setupActionListener(accountActionListener, concreteNotificaton.getAccount().getId()); } + break; } default: } @@ -540,8 +541,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter { } contentWarningButton.setOnClickListener(view -> { - if (getAdapterPosition() != RecyclerView.NO_POSITION) { - notificationActionListener.onExpandedChange(!statusViewData.isExpanded(), getAdapterPosition()); + if (getBindingAdapterPosition() != RecyclerView.NO_POSITION) { + notificationActionListener.onExpandedChange(!statusViewData.isExpanded(), getBindingAdapterPosition()); } statusContent.setVisibility(statusViewData.isExpanded() ? View.GONE : View.VISIBLE); }); @@ -618,7 +619,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter { if (statusViewData.isCollapsible() && (statusViewData.isExpanded() || !hasSpoiler)) { contentCollapseButton.setOnClickListener(view -> { - int position = getAdapterPosition(); + int position = getBindingAdapterPosition(); if (position != RecyclerView.NO_POSITION && notificationActionListener != null) { notificationActionListener.onNotificationContentCollapsedChange(!statusViewData.isCollapsed(), position); } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/PlaceholderViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/PlaceholderViewHolder.java index 2f946108..f8f1a0b5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/PlaceholderViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/PlaceholderViewHolder.java @@ -41,7 +41,7 @@ public final class PlaceholderViewHolder extends RecyclerView.ViewHolder { loadMoreButton.setEnabled(true); loadMoreButton.setOnClickListener(v -> { loadMoreButton.setEnabled(false); - listener.onLoadMore(getAdapterPosition()); + listener.onLoadMore(getBindingAdapterPosition()); }); } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt index 0208b953..1f57cc4e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt @@ -18,19 +18,18 @@ package com.keylesspalace.tusky.adapter import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.CheckBox -import android.widget.RadioButton -import android.widget.TextView import androidx.emoji.text.EmojiCompat import androidx.recyclerview.widget.RecyclerView -import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ItemPollBinding import com.keylesspalace.tusky.entity.Emoji -import com.keylesspalace.tusky.util.* +import com.keylesspalace.tusky.util.BindingHolder +import com.keylesspalace.tusky.util.emojify +import com.keylesspalace.tusky.util.visible import com.keylesspalace.tusky.viewdata.PollOptionViewData import com.keylesspalace.tusky.viewdata.buildDescription import com.keylesspalace.tusky.viewdata.calculatePercent -class PollAdapter: RecyclerView.Adapter() { +class PollAdapter: RecyclerView.Adapter>() { private var pollOptions: List = emptyList() private var voteCount: Int = 0 @@ -64,51 +63,54 @@ class PollAdapter: RecyclerView.Adapter() { } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PollViewHolder { - return PollViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_poll, parent, false)) + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder { + val binding = ItemPollBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return BindingHolder(binding) } - override fun getItemCount(): Int { - return pollOptions.size - } + override fun getItemCount() = pollOptions.size - override fun onBindViewHolder(holder: PollViewHolder, position: Int) { + override fun onBindViewHolder(holder: BindingHolder, position: Int) { val option = pollOptions[position] - holder.resultTextView.visible(mode == RESULT) - holder.radioButton.visible(mode == SINGLE) - holder.checkBox.visible(mode == MULTIPLE) + val resultTextView = holder.binding.statusPollOptionResult + val radioButton = holder.binding.statusPollRadioButton + val checkBox = holder.binding.statusPollCheckbox + + resultTextView.visible(mode == RESULT) + radioButton.visible(mode == SINGLE) + checkBox.visible(mode == MULTIPLE) when(mode) { RESULT -> { val percent = calculatePercent(option.votesCount, votersCount, voteCount) - val emojifiedPollOptionText = buildDescription(option.title, percent, holder.resultTextView.context) - .emojify(emojis, holder.resultTextView, animateEmojis) - holder.resultTextView.text = EmojiCompat.get().process(emojifiedPollOptionText) + val emojifiedPollOptionText = buildDescription(option.title, percent, resultTextView.context) + .emojify(emojis, resultTextView, animateEmojis) + resultTextView.text = EmojiCompat.get().process(emojifiedPollOptionText) val level = percent * 100 - holder.resultTextView.background.level = level - holder.resultTextView.setOnClickListener(resultClickListener) + resultTextView.background.level = level + resultTextView.setOnClickListener(resultClickListener) } SINGLE -> { - val emojifiedPollOptionText = option.title.emojify(emojis, holder.radioButton, animateEmojis) - holder.radioButton.text = EmojiCompat.get().process(emojifiedPollOptionText) - holder.radioButton.isChecked = option.selected - holder.radioButton.setOnClickListener { + val emojifiedPollOptionText = option.title.emojify(emojis, radioButton, animateEmojis) + radioButton.text = EmojiCompat.get().process(emojifiedPollOptionText) + radioButton.isChecked = option.selected + radioButton.setOnClickListener { pollOptions.forEachIndexed { index, pollOption -> - pollOption.selected = index == holder.adapterPosition + pollOption.selected = index == holder.bindingAdapterPosition notifyItemChanged(index) } } } MULTIPLE -> { - val emojifiedPollOptionText = option.title.emojify(emojis, holder.checkBox, animateEmojis) - holder.checkBox.text = EmojiCompat.get().process(emojifiedPollOptionText) - holder.checkBox.isChecked = option.selected - holder.checkBox.setOnCheckedChangeListener { _, isChecked -> - pollOptions[holder.adapterPosition].selected = isChecked + val emojifiedPollOptionText = option.title.emojify(emojis, checkBox, animateEmojis) + checkBox.text = EmojiCompat.get().process(emojifiedPollOptionText) + checkBox.isChecked = option.selected + checkBox.setOnCheckedChangeListener { _, isChecked -> + pollOptions[holder.bindingAdapterPosition].selected = isChecked } } } @@ -121,13 +123,3 @@ class PollAdapter: RecyclerView.Adapter() { const val MULTIPLE = 2 } } - - - -class PollViewHolder(view: View): RecyclerView.ViewHolder(view) { - - val resultTextView: TextView = view.findViewById(R.id.status_poll_option_result) - val radioButton: RadioButton = view.findViewById(R.id.status_poll_radio_button) - val checkBox: CheckBox = view.findViewById(R.id.status_poll_checkbox) - -} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/PreviewPollOptionsAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/PreviewPollOptionsAdapter.kt index 328e9626..4206f7cf 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/PreviewPollOptionsAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/PreviewPollOptionsAdapter.kt @@ -63,5 +63,4 @@ class PreviewPollOptionsAdapter: RecyclerView.Adapter() { } - -class PreviewViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) \ No newline at end of file +class PreviewViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/SavedTootAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/SavedTootAdapter.java index 6d4889c8..af9c31d5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/SavedTootAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/SavedTootAdapter.java @@ -113,9 +113,9 @@ public class SavedTootAdapter extends RecyclerView.Adapter { suppr.setOnClickListener(v -> { v.setEnabled(false); - handler.delete(getAdapterPosition(), item); + handler.delete(getBindingAdapterPosition(), item); }); - view.setOnClickListener(v -> handler.click(getAdapterPosition(), item)); + view.setOnClickListener(v -> handler.click(getBindingAdapterPosition(), item)); } } } 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 3fc27d7c..d6cee626 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java @@ -217,8 +217,8 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { setContentWarningButtonText(expanded); contentWarningButton.setOnClickListener(view -> { contentWarningDescription.invalidate(); - if (getAdapterPosition() != RecyclerView.NO_POSITION) { - listener.onExpandedChange(!expanded, getAdapterPosition()); + if (getBindingAdapterPosition() != RecyclerView.NO_POSITION) { + listener.onExpandedChange(!expanded, getBindingAdapterPosition()); } setContentWarningButtonText(!expanded); @@ -513,15 +513,15 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { sensitiveMediaWarning.setVisibility(showingContent ? View.GONE : View.VISIBLE); sensitiveMediaShow.setVisibility(showingContent ? View.VISIBLE : View.GONE); sensitiveMediaShow.setOnClickListener(v -> { - if (getAdapterPosition() != RecyclerView.NO_POSITION) { - listener.onContentHiddenChange(false, getAdapterPosition()); + if (getBindingAdapterPosition() != RecyclerView.NO_POSITION) { + listener.onContentHiddenChange(false, getBindingAdapterPosition()); } v.setVisibility(View.GONE); sensitiveMediaWarning.setVisibility(View.VISIBLE); }); sensitiveMediaWarning.setOnClickListener(v -> { - if (getAdapterPosition() != RecyclerView.NO_POSITION) { - listener.onContentHiddenChange(true, getAdapterPosition()); + if (getBindingAdapterPosition() != RecyclerView.NO_POSITION) { + listener.onContentHiddenChange(true, getBindingAdapterPosition()); } v.setVisibility(View.GONE); sensitiveMediaShow.setVisibility(View.VISIBLE); @@ -582,10 +582,10 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { private void setAttachmentClickListener(View view, StatusActionListener listener, int index, Attachment attachment, boolean animateTransition) { view.setOnClickListener(v -> { - int position = getAdapterPosition(); + int position = getBindingAdapterPosition(); if (position != RecyclerView.NO_POSITION) { if (sensitiveMediaWarning.getVisibility() == View.VISIBLE) { - listener.onContentHiddenChange(true, getAdapterPosition()); + listener.onContentHiddenChange(true, getBindingAdapterPosition()); } else { listener.onViewMedia(position, index, animateTransition ? v : null); } @@ -627,7 +627,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { displayName.setOnClickListener(profileButtonClickListener); replyButton.setOnClickListener(v -> { - int position = getAdapterPosition(); + int position = getBindingAdapterPosition(); if (position != RecyclerView.NO_POSITION) { listener.onReply(position); } @@ -635,7 +635,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { if (reblogButton != null) { reblogButton.setEventListener((button, buttonState) -> { // return true to play animaion - int position = getAdapterPosition(); + int position = getBindingAdapterPosition(); if (position != RecyclerView.NO_POSITION) { if (statusDisplayOptions.confirmReblogs()) { showConfirmReblogDialog(listener, statusContent, buttonState, position); @@ -651,7 +651,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { } favouriteButton.setEventListener((button, buttonState) -> { - int position = getAdapterPosition(); + int position = getBindingAdapterPosition(); if (position != RecyclerView.NO_POSITION) { listener.onFavourite(!buttonState, position); } @@ -659,7 +659,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { }); bookmarkButton.setEventListener((button, buttonState) -> { - int position = getAdapterPosition(); + int position = getBindingAdapterPosition(); if (position != RecyclerView.NO_POSITION) { listener.onBookmark(!buttonState, position); } @@ -667,7 +667,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { }); moreButton.setOnClickListener(v -> { - int position = getAdapterPosition(); + int position = getBindingAdapterPosition(); if (position != RecyclerView.NO_POSITION) { listener.onMore(v, position); } @@ -677,7 +677,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { * just eat the clicks instead of deferring to the parent listener, but WILL respond to a * listener directly on the TextView, for whatever reason. */ View.OnClickListener viewThreadListener = v -> { - int position = getAdapterPosition(); + int position = getBindingAdapterPosition(); if (position != RecyclerView.NO_POSITION) { listener.onViewThread(position); } @@ -926,7 +926,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { if (expired || poll.getVoted()) { // no voting possible View.OnClickListener viewThreadListener = v -> { - int position = getAdapterPosition(); + int position = getBindingAdapterPosition(); if (position != RecyclerView.NO_POSITION) { listener.onViewThread(position); } @@ -958,7 +958,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { pollButton.setOnClickListener(v -> { - int position = getAdapterPosition(); + int position = getBindingAdapterPosition(); if (position != RecyclerView.NO_POSITION) { diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java index 8755e8e8..abb8ca85 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java @@ -72,13 +72,13 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder { } reblogs.setOnClickListener(v -> { - int position = getAdapterPosition(); + int position = getBindingAdapterPosition(); if (position != RecyclerView.NO_POSITION) { listener.onShowReblogs(position); } }); favourites.setOnClickListener(v -> { - int position = getAdapterPosition(); + int position = getBindingAdapterPosition(); if (position != RecyclerView.NO_POSITION) { listener.onShowFavs(position); } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java index 043b7b35..68d64a69 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java @@ -67,7 +67,7 @@ public class StatusViewHolder extends StatusBaseViewHolder { hideStatusInfo(); } else { setRebloggedByDisplayName(rebloggedByDisplayName, status, statusDisplayOptions); - statusInfo.setOnClickListener(v -> listener.onOpenReblog(getAdapterPosition())); + statusInfo.setOnClickListener(v -> listener.onOpenReblog(getBindingAdapterPosition())); } } @@ -105,7 +105,7 @@ public class StatusViewHolder extends StatusBaseViewHolder { /* input filter for TextViews have to be set before text */ if (status.isCollapsible() && (status.isExpanded() || TextUtils.isEmpty(status.getSpoilerText()))) { contentCollapseButton.setOnClickListener(view -> { - int position = getAdapterPosition(); + int position = getBindingAdapterPosition(); if (position != RecyclerView.NO_POSITION) listener.onContentCollapsedChange(!status.isCollapsed(), position); }); diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt index b4517dc6..bec07f06 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt @@ -18,19 +18,21 @@ package com.keylesspalace.tusky.adapter import android.content.res.ColorStateList import android.view.LayoutInflater import android.view.MotionEvent -import android.view.View import android.view.ViewGroup import androidx.core.view.size import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding import com.google.android.material.chip.Chip import com.keylesspalace.tusky.HASHTAG import com.keylesspalace.tusky.LIST import com.keylesspalace.tusky.R import com.keylesspalace.tusky.TabData +import com.keylesspalace.tusky.databinding.ItemTabPreferenceBinding +import com.keylesspalace.tusky.databinding.ItemTabPreferenceSmallBinding +import com.keylesspalace.tusky.util.BindingHolder import com.keylesspalace.tusky.util.ThemeUtils import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.show -import kotlinx.android.synthetic.main.item_tab_preference.view.* interface ItemInteractionListener { fun onTabAdded(tab: TabData) @@ -44,61 +46,69 @@ interface ItemInteractionListener { class TabAdapter(private var data: List, private val small: Boolean, private val listener: ItemInteractionListener, - private var removeButtonEnabled: Boolean = false) : RecyclerView.Adapter() { + private var removeButtonEnabled: Boolean = false +) : RecyclerView.Adapter>() { fun updateData(newData: List) { this.data = newData notifyDataSetChanged() } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val layoutId = if (small) { - R.layout.item_tab_preference_small + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder { + val binding = if (small) { + ItemTabPreferenceSmallBinding.inflate(LayoutInflater.from(parent.context), parent, false) } else { - R.layout.item_tab_preference + ItemTabPreferenceBinding.inflate(LayoutInflater.from(parent.context), parent, false) } - val view = LayoutInflater.from(parent.context).inflate(layoutId, parent, false) - return ViewHolder(view) + return BindingHolder(binding) } - override fun onBindViewHolder(holder: ViewHolder, position: Int) { + override fun onBindViewHolder(holder: BindingHolder, position: Int) { val context = holder.itemView.context val tab = data[position] - if (!small && tab.id == LIST) { - holder.itemView.textView.text = tab.arguments.getOrNull(1).orEmpty() - } else { - holder.itemView.textView.setText(tab.text) - } - holder.itemView.textView.setCompoundDrawablesRelativeWithIntrinsicBounds(tab.icon, 0, 0, 0) + if (small) { - holder.itemView.textView.setOnClickListener { + val binding = holder.binding as ItemTabPreferenceSmallBinding + + binding.textView.setText(tab.text) + + binding.textView.setCompoundDrawablesRelativeWithIntrinsicBounds(tab.icon, 0, 0, 0) + + binding.textView.setOnClickListener { listener.onTabAdded(tab) } - } - holder.itemView.imageView?.setOnTouchListener { _, event -> - if (event.action == MotionEvent.ACTION_DOWN) { - listener.onStartDrag(holder) - true + + } else { + val binding = holder.binding as ItemTabPreferenceBinding + + if (tab.id == LIST) { + binding.textView.text = tab.arguments.getOrNull(1).orEmpty() } else { - false + binding.textView.setText(tab.text) } - } - holder.itemView.removeButton?.setOnClickListener { - listener.onTabRemoved(holder.adapterPosition) - } - if (holder.itemView.removeButton != null) { - holder.itemView.removeButton.isEnabled = removeButtonEnabled + + binding.textView.setCompoundDrawablesRelativeWithIntrinsicBounds(tab.icon, 0, 0, 0) + + binding.imageView.setOnTouchListener { _, event -> + if (event.action == MotionEvent.ACTION_DOWN) { + listener.onStartDrag(holder) + true + } else { + false + } + } + binding.removeButton.setOnClickListener { + listener.onTabRemoved(holder.bindingAdapterPosition) + } + binding.removeButton.isEnabled = removeButtonEnabled ThemeUtils.setDrawableTint( holder.itemView.context, - holder.itemView.removeButton.drawable, + binding.removeButton.drawable, (if (removeButtonEnabled) android.R.attr.textColorTertiary else R.attr.textColorDisabled) ) - } - - if (!small) { if (tab.id == HASHTAG) { - holder.itemView.chipGroup.show() + binding.chipGroup.show() /* * The chip group will always contain the actionChip (it is defined in the xml layout). @@ -107,9 +117,9 @@ class TabAdapter(private var data: List, */ tab.arguments.forEachIndexed { i, arg -> - val chip = holder.itemView.chipGroup.getChildAt(i).takeUnless { it.id == R.id.actionChip } as Chip? + val chip = binding.chipGroup.getChildAt(i).takeUnless { it.id == R.id.actionChip } as Chip? ?: Chip(context).apply { - holder.itemView.chipGroup.addView(this, holder.itemView.chipGroup.size - 1) + binding.chipGroup.addView(this, binding.chipGroup.size - 1) chipIconTint = ColorStateList.valueOf(ThemeUtils.getColor(context, android.R.attr.textColorPrimary)) } @@ -121,21 +131,21 @@ class TabAdapter(private var data: List, } else { chip.setChipIconResource(R.drawable.ic_cancel_24dp) chip.setOnClickListener { - listener.onChipClicked(tab, holder.adapterPosition, i) + listener.onChipClicked(tab, holder.bindingAdapterPosition, i) } } } - while(holder.itemView.chipGroup.size - 1 > tab.arguments.size) { - holder.itemView.chipGroup.removeViewAt(tab.arguments.size) + while(binding.chipGroup.size - 1 > tab.arguments.size) { + binding.chipGroup.removeViewAt(tab.arguments.size) } - holder.itemView.actionChip.setOnClickListener { - listener.onActionChipClicked(tab, holder.adapterPosition) + binding.actionChip.setOnClickListener { + listener.onActionChipClicked(tab, holder.bindingAdapterPosition) } } else { - holder.itemView.chipGroup.hide() + binding.chipGroup.hide() } } } @@ -148,6 +158,4 @@ class TabAdapter(private var data: List, notifyDataSetChanged() } } - - class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementAdapter.kt index b54b1555..5014b52e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementAdapter.kt @@ -19,19 +19,17 @@ import android.view.ContextThemeWrapper import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.TextView import androidx.core.view.size import androidx.recyclerview.widget.RecyclerView import com.google.android.material.chip.Chip -import com.google.android.material.chip.ChipGroup import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ItemAnnouncementBinding import com.keylesspalace.tusky.entity.Announcement import com.keylesspalace.tusky.entity.Emoji import com.keylesspalace.tusky.interfaces.LinkListener +import com.keylesspalace.tusky.util.BindingHolder import com.keylesspalace.tusky.util.LinkHelper import com.keylesspalace.tusky.util.emojify -import kotlinx.android.synthetic.main.item_announcement.view.* - interface AnnouncementActionListener: LinkListener { fun openReactionPicker(announcementId: String, target: View) @@ -44,16 +42,74 @@ class AnnouncementAdapter( private val listener: AnnouncementActionListener, private val wellbeingEnabled: Boolean = false, private val animateEmojis: Boolean = false -) : RecyclerView.Adapter() { +) : RecyclerView.Adapter>() { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AnnouncementViewHolder { - val view = LayoutInflater.from(parent.context) - .inflate(R.layout.item_announcement, parent, false) - return AnnouncementViewHolder(view) + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder { + val binding = ItemAnnouncementBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return BindingHolder(binding) } - override fun onBindViewHolder(viewHolder: AnnouncementViewHolder, position: Int) { - viewHolder.bind(items[position]) + override fun onBindViewHolder(holder: BindingHolder, position: Int) { + val item = items[position] + + val text = holder.binding.text + val chips = holder.binding.chipGroup + val addReactionChip = holder.binding.addReactionChip + + LinkHelper.setClickableText(text, item.content, null, listener) + + // If wellbeing mode is enabled, announcement badge counts should not be shown. + if (wellbeingEnabled) { + // Since reactions are not visible in wellbeing mode, + // we shouldn't be able to add any ourselves. + addReactionChip.visibility = View.GONE + return + } + + item.reactions.forEachIndexed { i, reaction -> + (chips.getChildAt(i)?.takeUnless { it.id == R.id.addReactionChip } as Chip? + ?: Chip(ContextThemeWrapper(chips.context, R.style.Widget_MaterialComponents_Chip_Choice)).apply { + isCheckable = true + checkedIcon = null + chips.addView(this, i) + }) + .apply { + val emojiText = if (reaction.url == null) { + reaction.name + } else { + context.getString(R.string.emoji_shortcode_format, reaction.name) + } + this.text = ("$emojiText ${reaction.count}") + .emojify( + listOf(Emoji( + reaction.name, + reaction.url ?: "", + reaction.staticUrl ?: "", + null + )), + this, + animateEmojis + ) + + isChecked = reaction.me + + setOnClickListener { + if (reaction.me) { + listener.removeReaction(item.id, reaction.name) + } else { + listener.addReaction(item.id, reaction.name) + } + } + } + } + + while (chips.size - 1 > item.reactions.size) { + chips.removeViewAt(item.reactions.size) + } + + addReactionChip.setOnClickListener { + listener.openReactionPicker(item.id, it) + } } override fun getItemCount() = items.size @@ -62,67 +118,4 @@ class AnnouncementAdapter( this.items = items notifyDataSetChanged() } - - inner class AnnouncementViewHolder(private val view: View) : RecyclerView.ViewHolder(view) { - private val text: TextView = view.text - private val chips: ChipGroup = view.chipGroup - private val addReactionChip: Chip = view.addReactionChip - - fun bind(item: Announcement) { - LinkHelper.setClickableText(text, item.content, null, listener) - - // If wellbeing mode is enabled, announcement badge counts should not be shown. - if (wellbeingEnabled) { - // Since reactions are not visible in wellbeing mode, - // we shouldn't be able to add any ourselves. - addReactionChip.visibility = View.GONE - return - } - - item.reactions.forEachIndexed { i, reaction -> - (chips.getChildAt(i)?.takeUnless { it.id == R.id.addReactionChip } as Chip? - ?: Chip(ContextThemeWrapper(view.context, R.style.Widget_MaterialComponents_Chip_Choice)).apply { - isCheckable = true - checkedIcon = null - chips.addView(this, i) - }) - .apply { - val emojiText = if (reaction.url == null) { - reaction.name - } else { - view.context.getString(R.string.emoji_shortcode_format, reaction.name) - } - text = ("$emojiText ${reaction.count}") - .emojify( - listOf(Emoji( - reaction.name, - reaction.url ?: "", - reaction.staticUrl ?: "", - null - )), - this, - animateEmojis - ) - - isChecked = reaction.me - - setOnClickListener { - if (reaction.me) { - listener.removeReaction(item.id, reaction.name) - } else { - listener.addReaction(item.id, reaction.name) - } - } - } - } - - while (chips.size - 1 > item.reactions.size) { - chips.removeViewAt(item.reactions.size) - } - - addReactionChip.setOnClickListener { - listener.openReactionPicker(item.id, it) - } - } - } } 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 fe6cf1ba..ada2769c 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 @@ -30,13 +30,12 @@ import com.keylesspalace.tusky.R import com.keylesspalace.tusky.ViewTagActivity import com.keylesspalace.tusky.adapter.EmojiAdapter import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener +import com.keylesspalace.tusky.databinding.ActivityAnnouncementsBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.util.* import com.keylesspalace.tusky.view.EmojiPicker -import kotlinx.android.synthetic.main.activity_announcements.* -import kotlinx.android.synthetic.main.toolbar_basic.* import javax.inject.Inject class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener, OnEmojiSelectedListener, Injectable { @@ -46,6 +45,8 @@ class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener, private val viewModel: AnnouncementsViewModel by viewModels { viewModelFactory } + private val binding by viewBinding(ActivityAnnouncementsBinding::inflate) + private lateinit var adapter: AnnouncementAdapter private val picker by lazy { EmojiPicker(this) } @@ -63,22 +64,22 @@ class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener, override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_announcements) + setContentView(binding.root) - setSupportActionBar(toolbar) + setSupportActionBar(binding.includedToolbar.toolbar) supportActionBar?.apply { title = getString(R.string.title_announcements) setDisplayHomeAsUpEnabled(true) setDisplayShowHomeEnabled(true) } - swipeRefreshLayout.setOnRefreshListener(this::refreshAnnouncements) - swipeRefreshLayout.setColorSchemeResources(R.color.chinwag_green) + binding.swipeRefreshLayout.setOnRefreshListener(this::refreshAnnouncements) + binding.swipeRefreshLayout.setColorSchemeResources(R.color.chinwag_green) - announcementsList.setHasFixedSize(true) - announcementsList.layoutManager = LinearLayoutManager(this) + binding.announcementsList.setHasFixedSize(true) + binding.announcementsList.layoutManager = LinearLayoutManager(this) val divider = DividerItemDecoration(this, DividerItemDecoration.VERTICAL) - announcementsList.addItemDecoration(divider) + binding.announcementsList.addItemDecoration(divider) val preferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) val wellbeingEnabled = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false) @@ -86,31 +87,31 @@ class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener, adapter = AnnouncementAdapter(emptyList(), this, wellbeingEnabled, animateEmojis) - announcementsList.adapter = adapter + binding.announcementsList.adapter = adapter viewModel.announcements.observe(this) { when (it) { is Success -> { - progressBar.hide() - swipeRefreshLayout.isRefreshing = false + binding.progressBar.hide() + binding.swipeRefreshLayout.isRefreshing = false if (it.data.isNullOrEmpty()) { - errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_announcements) - errorMessageView.show() + binding.errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_announcements) + binding.errorMessageView.show() } else { - errorMessageView.hide() + binding.errorMessageView.hide() } adapter.updateList(it.data ?: listOf()) } is Loading -> { - errorMessageView.hide() + binding.errorMessageView.hide() } is Error -> { - progressBar.hide() - swipeRefreshLayout.isRefreshing = false - errorMessageView.setup(R.drawable.elephant_error, R.string.error_generic) { + binding.progressBar.hide() + binding.swipeRefreshLayout.isRefreshing = false + binding.errorMessageView.setup(R.drawable.elephant_error, R.string.error_generic) { refreshAnnouncements() } - errorMessageView.show() + binding.errorMessageView.show() } } } @@ -120,12 +121,12 @@ class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener, } viewModel.load() - progressBar.show() + binding.progressBar.show() } private fun refreshAnnouncements() { viewModel.load() - swipeRefreshLayout.isRefreshing = true + binding.swipeRefreshLayout.isRefreshing = true } override fun openReactionPicker(announcementId: String, target: View) { 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 269a70c6..81f6c565 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 @@ -18,7 +18,6 @@ package com.keylesspalace.tusky.components.compose import android.Manifest import android.app.Activity import android.app.ProgressDialog -import android.app.TimePickerDialog import android.content.Context import android.content.Intent import android.content.SharedPreferences @@ -61,6 +60,8 @@ import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener import com.keylesspalace.tusky.components.compose.dialog.makeCaptionDialog import com.keylesspalace.tusky.components.compose.dialog.showAddPollDialog import com.keylesspalace.tusky.components.compose.view.ComposeOptionsListener +import com.keylesspalace.tusky.components.compose.view.ComposeScheduleView +import com.keylesspalace.tusky.databinding.ActivityComposeBinding import com.keylesspalace.tusky.db.AccountEntity import com.keylesspalace.tusky.db.DraftAttachment import com.keylesspalace.tusky.di.Injectable @@ -75,11 +76,10 @@ import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial import com.mikepenz.iconics.utils.colorInt import com.mikepenz.iconics.utils.sizeDp -import kotlinx.android.parcel.Parcelize -import kotlinx.android.synthetic.main.activity_compose.* +import kotlinx.parcelize.Parcelize import java.io.File import java.io.IOException -import java.util.* +import java.util.Locale import javax.inject.Inject import kotlin.math.max import kotlin.math.min @@ -90,7 +90,7 @@ class ComposeActivity : BaseActivity(), OnEmojiSelectedListener, Injectable, InputConnectionCompat.OnCommitContentListener, - TimePickerDialog.OnTimeSetListener { + ComposeScheduleView.OnTimeSetListener { @Inject lateinit var viewModelFactory: ViewModelFactory @@ -109,17 +109,20 @@ class ComposeActivity : BaseActivity(), private val viewModel: ComposeViewModel by viewModels { viewModelFactory } + private val binding by viewBinding(ActivityComposeBinding::inflate) + private val maxUploadMediaNumber = 4 private var mediaCount = 0 public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + val preferences = PreferenceManager.getDefaultSharedPreferences(this) val theme = preferences.getString("appTheme", ThemeUtils.APP_THEME_DEFAULT) if (theme == "black") { setTheme(R.style.TuskyDialogActivityBlackTheme) } - setContentView(R.layout.activity_compose) + setContentView(binding.root) setupActionBar() // do not do anything when not logged in, activity will be finished in super.onCreate() anyway @@ -135,10 +138,10 @@ class ComposeActivity : BaseActivity(), }, onRemove = this::removeMediaFromQueue ) - composeMediaPreviewBar.layoutManager = + binding.composeMediaPreviewBar.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false) - composeMediaPreviewBar.adapter = mediaAdapter - composeMediaPreviewBar.itemAnimator = null + binding.composeMediaPreviewBar.adapter = mediaAdapter + binding.composeMediaPreviewBar.itemAnimator = null subscribeToUpdates(mediaAdapter) setupButtons() @@ -154,11 +157,11 @@ class ComposeActivity : BaseActivity(), setupReplyViews(composeOptions?.replyingStatusAuthor, composeOptions?.replyingStatusContent) val tootText = composeOptions?.tootText if (!tootText.isNullOrEmpty()) { - composeEditField.setText(tootText) + binding.composeEditField.setText(tootText) } if (!composeOptions?.scheduledAt.isNullOrEmpty()) { - composeScheduleView.setDateTime(composeOptions?.scheduledAt) + binding.composeScheduleView.setDateTime(composeOptions?.scheduledAt) } setupComposeField(preferences, viewModel.startingText) @@ -198,14 +201,14 @@ class ComposeActivity : BaseActivity(), } if (shareBody.isNotBlank()) { - val start = composeEditField.selectionStart.coerceAtLeast(0) - val end = composeEditField.selectionEnd.coerceAtLeast(0) + val start = binding.composeEditField.selectionStart.coerceAtLeast(0) + val end = binding.composeEditField.selectionEnd.coerceAtLeast(0) val left = min(start, end) val right = max(start, end) - composeEditField.text.replace(left, right, shareBody, 0, shareBody.length) + binding.composeEditField.text.replace(left, right, shareBody, 0, shareBody.length) // move edittext cursor to first when shareBody parsed - composeEditField.text.insert(0, "\n") - composeEditField.setSelection(0) + binding.composeEditField.text.insert(0, "\n") + binding.composeEditField.setSelection(0) } } } @@ -214,58 +217,58 @@ class ComposeActivity : BaseActivity(), private fun setupReplyViews(replyingStatusAuthor: String?, replyingStatusContent: String?) { if (replyingStatusAuthor != null) { - composeReplyView.show() - composeReplyView.text = getString(R.string.replying_to, replyingStatusAuthor) + binding.composeReplyView.show() + binding.composeReplyView.text = getString(R.string.replying_to, replyingStatusAuthor) val arrowDownIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_arrow_drop_down).apply { sizeDp = 12 } ThemeUtils.setDrawableTint(this, arrowDownIcon, android.R.attr.textColorTertiary) - composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, arrowDownIcon, null) + binding.composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, arrowDownIcon, null) - composeReplyView.setOnClickListener { - TransitionManager.beginDelayedTransition(composeReplyContentView.parent as ViewGroup) + binding.composeReplyView.setOnClickListener { + TransitionManager.beginDelayedTransition(binding.composeReplyContentView.parent as ViewGroup) - if (composeReplyContentView.isVisible) { - composeReplyContentView.hide() - composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, arrowDownIcon, null) + if (binding.composeReplyContentView.isVisible) { + binding.composeReplyContentView.hide() + binding.composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, arrowDownIcon, null) } else { - composeReplyContentView.show() + binding.composeReplyContentView.show() val arrowUpIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_arrow_drop_up).apply { sizeDp = 12 } ThemeUtils.setDrawableTint(this, arrowUpIcon, android.R.attr.textColorTertiary) - composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, arrowUpIcon, null) + binding.composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, arrowUpIcon, null) } } } - replyingStatusContent?.let { composeReplyContentView.text = it } + replyingStatusContent?.let { binding.composeReplyContentView.text = it } } private fun setupContentWarningField(startingContentWarning: String?) { if (startingContentWarning != null) { - composeContentWarningField.setText(startingContentWarning) + binding.composeContentWarningField.setText(startingContentWarning) } - composeContentWarningField.onTextChanged { _, _, _, _ -> updateVisibleCharactersLeft() } + binding.composeContentWarningField.onTextChanged { _, _, _, _ -> updateVisibleCharactersLeft() } } private fun setupComposeField(preferences: SharedPreferences, startingText: String?) { - composeEditField.setOnCommitContentListener(this) + binding.composeEditField.setOnCommitContentListener(this) - composeEditField.setOnKeyListener { _, keyCode, event -> this.onKeyDown(keyCode, event) } + binding.composeEditField.setOnKeyListener { _, keyCode, event -> this.onKeyDown(keyCode, event) } - composeEditField.setAdapter( + binding.composeEditField.setAdapter( ComposeAutoCompleteAdapter( this, preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false), preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) ) ) - composeEditField.setTokenizer(ComposeTokenizer()) + binding.composeEditField.setTokenizer(ComposeTokenizer()) - composeEditField.setText(startingText) - composeEditField.setSelection(composeEditField.length()) + binding.composeEditField.setText(startingText) + binding.composeEditField.setSelection(binding.composeEditField.length()) - val mentionColour = composeEditField.linkTextColors.defaultColor - highlightSpans(composeEditField.text, mentionColour) - composeEditField.afterTextChanged { editable -> + val mentionColour = binding.composeEditField.linkTextColors.defaultColor + highlightSpans(binding.composeEditField.text, mentionColour) + binding.composeEditField.afterTextChanged { editable -> highlightSpans(editable, mentionColour) updateVisibleCharactersLeft() } @@ -273,7 +276,7 @@ class ComposeActivity : BaseActivity(), // work around Android platform bug -> https://issuetracker.google.com/issues/67102093 if (Build.VERSION.SDK_INT == Build.VERSION_CODES.O || Build.VERSION.SDK_INT == Build.VERSION_CODES.O_MR1) { - composeEditField.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + binding.composeEditField.setLayerType(View.LAYER_TYPE_SOFTWARE, null) } } @@ -282,7 +285,7 @@ class ComposeActivity : BaseActivity(), viewModel.instanceParams.observe { instanceData -> maximumTootCharacters = instanceData.maxChars updateVisibleCharactersLeft() - composeScheduleButton.visible(instanceData.supportsScheduled) + binding.composeScheduleButton.visible(instanceData.supportsScheduled) } viewModel.emoji.observe { emoji -> setEmojiList(emoji) } combineLiveData(viewModel.markMediaAsSensitive, viewModel.showContentWarning) { markSensitive, showContentWarning -> @@ -296,19 +299,19 @@ class ComposeActivity : BaseActivity(), mediaAdapter.submitList(media) if (media.size != mediaCount) { mediaCount = media.size - composeMediaPreviewBar.visible(media.isNotEmpty()) + binding.composeMediaPreviewBar.visible(media.isNotEmpty()) updateSensitiveMediaToggle(viewModel.markMediaAsSensitive.value != false, viewModel.showContentWarning.value != false) } } viewModel.poll.observe { poll -> - pollPreview.visible(poll != null) - poll?.let(pollPreview::setPoll) + binding.pollPreview.visible(poll != null) + poll?.let(binding.pollPreview::setPoll) } viewModel.scheduledAt.observe { scheduledAt -> if (scheduledAt == null) { - composeScheduleView.resetSchedule() + binding.composeScheduleView.resetSchedule() } else { - composeScheduleView.setDateTime(scheduledAt) + binding.composeScheduleView.setDateTime(scheduledAt) } updateScheduleButton() } @@ -316,7 +319,7 @@ class ComposeActivity : BaseActivity(), val active = poll == null && media!!.size != 4 && (media.isEmpty() || media.first().type == QueuedMedia.Type.IMAGE) - enableButton(composeAddMediaButton, active, active) + enableButton(binding.composeAddMediaButton, active, active) enablePollButton(media.isNullOrEmpty()) }.subscribe() viewModel.uploadError.observe { @@ -324,51 +327,52 @@ class ComposeActivity : BaseActivity(), } viewModel.setupComplete.observe { // Focus may have changed during view model setup, ensure initial focus is on the edit field - composeEditField.requestFocus() + binding.composeEditField.requestFocus() } } } private fun setupButtons() { - composeOptionsBottomSheet.listener = this + binding.composeOptionsBottomSheet.listener = this - composeOptionsBehavior = BottomSheetBehavior.from(composeOptionsBottomSheet) - addMediaBehavior = BottomSheetBehavior.from(addMediaBottomSheet) - scheduleBehavior = BottomSheetBehavior.from(composeScheduleView) - emojiBehavior = BottomSheetBehavior.from(emojiView) + composeOptionsBehavior = BottomSheetBehavior.from(binding.composeOptionsBottomSheet) + addMediaBehavior = BottomSheetBehavior.from(binding.addMediaBottomSheet) + scheduleBehavior = BottomSheetBehavior.from(binding.composeScheduleView) + emojiBehavior = BottomSheetBehavior.from(binding.emojiView) - enableButton(composeEmojiButton, clickable = false, colorActive = false) + enableButton(binding.composeEmojiButton, clickable = false, colorActive = false) // Setup the interface buttons. - composeTootButton.setOnClickListener { onSendClicked() } - composeAddMediaButton.setOnClickListener { openPickDialog() } - composeToggleVisibilityButton.setOnClickListener { showComposeOptions() } - composeContentWarningButton.setOnClickListener { onContentWarningChanged() } - composeEmojiButton.setOnClickListener { showEmojis() } - composeHideMediaButton.setOnClickListener { toggleHideMedia() } - composeScheduleButton.setOnClickListener { onScheduleClick() } - composeScheduleView.setResetOnClickListener { resetSchedule() } - atButton.setOnClickListener { atButtonClicked() } - hashButton.setOnClickListener { hashButtonClicked() } + binding.composeTootButton.setOnClickListener { onSendClicked() } + binding.composeAddMediaButton.setOnClickListener { openPickDialog() } + binding.composeToggleVisibilityButton.setOnClickListener { showComposeOptions() } + binding.composeContentWarningButton.setOnClickListener { onContentWarningChanged() } + binding.composeEmojiButton.setOnClickListener { showEmojis() } + binding.composeHideMediaButton.setOnClickListener { toggleHideMedia() } + binding.composeScheduleButton.setOnClickListener { onScheduleClick() } + binding.composeScheduleView.setResetOnClickListener { resetSchedule() } + binding.composeScheduleView.setListener(this) + binding.atButton.setOnClickListener { atButtonClicked() } + binding.hashButton.setOnClickListener { hashButtonClicked() } val textColor = ThemeUtils.getColor(this, android.R.attr.textColorTertiary) val cameraIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_camera_alt).apply { colorInt = textColor; sizeDp = 18 } - actionPhotoTake.setCompoundDrawablesRelativeWithIntrinsicBounds(cameraIcon, null, null, null) + binding.actionPhotoTake.setCompoundDrawablesRelativeWithIntrinsicBounds(cameraIcon, null, null, null) val imageIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_image).apply { colorInt = textColor; sizeDp = 18 } - actionPhotoPick.setCompoundDrawablesRelativeWithIntrinsicBounds(imageIcon, null, null, null) + binding.actionPhotoPick.setCompoundDrawablesRelativeWithIntrinsicBounds(imageIcon, null, null, null) val pollIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_poll).apply { colorInt = textColor; sizeDp = 18 } - addPollTextActionTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(pollIcon, null, null, null) + binding.addPollTextActionTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(pollIcon, null, null, null) - actionPhotoTake.setOnClickListener { initiateCameraApp() } - actionPhotoPick.setOnClickListener { onMediaPick() } - addPollTextActionTextView.setOnClickListener { openPollDialog() } + binding.actionPhotoTake.setOnClickListener { initiateCameraApp() } + binding.actionPhotoPick.setOnClickListener { onMediaPick() } + binding.addPollTextActionTextView.setOnClickListener { openPollDialog() } } private fun setupActionBar() { - setSupportActionBar(toolbar) + setSupportActionBar(binding.toolbar) supportActionBar?.run { title = null setDisplayHomeAsUpEnabled(true) @@ -387,40 +391,40 @@ class ComposeActivity : BaseActivity(), val animateAvatars = preferences.getBoolean("animateGifAvatars", false) loadAvatar( activeAccount.profilePictureUrl, - composeAvatar, + binding.composeAvatar, avatarSize / 8, animateAvatars ) - composeAvatar.contentDescription = getString(R.string.compose_active_account_description, + binding.composeAvatar.contentDescription = getString(R.string.compose_active_account_description, activeAccount.fullName) } private fun replaceTextAtCaret(text: CharSequence) { // If you select "backward" in an editable, you get SelectionStart > SelectionEnd - val start = composeEditField.selectionStart.coerceAtMost(composeEditField.selectionEnd) - val end = composeEditField.selectionStart.coerceAtLeast(composeEditField.selectionEnd) - val textToInsert = if (start > 0 && !composeEditField.text[start - 1].isWhitespace()) { + val start = binding.composeEditField.selectionStart.coerceAtMost(binding.composeEditField.selectionEnd) + val end = binding.composeEditField.selectionStart.coerceAtLeast(binding.composeEditField.selectionEnd) + val textToInsert = if (start > 0 && !binding.composeEditField.text[start - 1].isWhitespace()) { " $text" } else { text } - composeEditField.text.replace(start, end, textToInsert) + binding.composeEditField.text.replace(start, end, textToInsert) // Set the cursor after the inserted text - composeEditField.setSelection(start + text.length) + binding.composeEditField.setSelection(start + text.length) } fun prependSelectedWordsWith(text: CharSequence) { // If you select "backward" in an editable, you get SelectionStart > SelectionEnd - val start = composeEditField.selectionStart.coerceAtMost(composeEditField.selectionEnd) - val end = composeEditField.selectionStart.coerceAtLeast(composeEditField.selectionEnd) - val editorText = composeEditField.text + val start = binding.composeEditField.selectionStart.coerceAtMost(binding.composeEditField.selectionEnd) + val end = binding.composeEditField.selectionStart.coerceAtLeast(binding.composeEditField.selectionEnd) + val editorText = binding.composeEditField.text if (start == end) { // No selection, just insert text at caret editorText.insert(start, text) // Set the cursor after the inserted text - composeEditField.setSelection(start + text.length) + binding.composeEditField.setSelection(start + text.length) } else { var wasWord: Boolean var isWord = end < editorText.length && !Character.isWhitespace(editorText[end]) @@ -446,7 +450,7 @@ class ComposeActivity : BaseActivity(), } // Keep the same text (including insertions) selected - composeEditField.setSelection(start, newEnd) + binding.composeEditField.setSelection(start, newEnd) } } @@ -465,7 +469,7 @@ class ComposeActivity : BaseActivity(), } private fun displayTransientError(@StringRes stringId: Int) { - val bar = Snackbar.make(activityCompose, stringId, Snackbar.LENGTH_LONG) + val bar = Snackbar.make(binding.activityCompose, stringId, Snackbar.LENGTH_LONG) //necessary so snackbar is shown over everything bar.view.elevation = resources.getDimension(R.dimen.compose_activity_snackbar_elevation) bar.show() @@ -477,49 +481,49 @@ class ComposeActivity : BaseActivity(), private fun updateSensitiveMediaToggle(markMediaSensitive: Boolean, contentWarningShown: Boolean) { if (viewModel.media.value.isNullOrEmpty()) { - composeHideMediaButton.hide() + binding.composeHideMediaButton.hide() } else { - composeHideMediaButton.show() + binding.composeHideMediaButton.show() @ColorInt val color = if (contentWarningShown) { - composeHideMediaButton.setImageResource(R.drawable.ic_hide_media_24dp) - composeHideMediaButton.isClickable = false + binding.composeHideMediaButton.setImageResource(R.drawable.ic_hide_media_24dp) + binding.composeHideMediaButton.isClickable = false ContextCompat.getColor(this, R.color.transparent_chinwag_green) } else { - composeHideMediaButton.isClickable = true + binding.composeHideMediaButton.isClickable = true if (markMediaSensitive) { - composeHideMediaButton.setImageResource(R.drawable.ic_hide_media_24dp) + binding.composeHideMediaButton.setImageResource(R.drawable.ic_hide_media_24dp) ContextCompat.getColor(this, R.color.chinwag_green) } else { - composeHideMediaButton.setImageResource(R.drawable.ic_eye_24dp) + binding.composeHideMediaButton.setImageResource(R.drawable.ic_eye_24dp) ThemeUtils.getColor(this, android.R.attr.textColorTertiary) } } - composeHideMediaButton.drawable.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN) + binding.composeHideMediaButton.drawable.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN) } } private fun updateScheduleButton() { - @ColorInt val color = if (composeScheduleView.time == null) { + @ColorInt val color = if (binding.composeScheduleView.time == null) { ThemeUtils.getColor(this, android.R.attr.textColorTertiary) } else { ContextCompat.getColor(this, R.color.chinwag_green) } - composeScheduleButton.drawable.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN) + binding.composeScheduleButton.drawable.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN) } private fun enableButtons(enable: Boolean) { - composeAddMediaButton.isClickable = enable - composeToggleVisibilityButton.isClickable = enable - composeEmojiButton.isClickable = enable - composeHideMediaButton.isClickable = enable - composeScheduleButton.isClickable = enable - composeTootButton.isEnabled = enable + binding.composeAddMediaButton.isClickable = enable + binding.composeToggleVisibilityButton.isClickable = enable + binding.composeEmojiButton.isClickable = enable + binding.composeHideMediaButton.isClickable = enable + binding.composeScheduleButton.isClickable = enable + binding.composeTootButton.isEnabled = enable } private fun setStatusVisibility(visibility: Status.Visibility) { - composeOptionsBottomSheet.setStatusVisibility(visibility) - composeTootButton.setStatusVisibility(visibility) + binding.composeOptionsBottomSheet.setStatusVisibility(visibility) + binding.composeTootButton.setStatusVisibility(visibility) val iconRes = when (visibility) { Status.Visibility.PUBLIC -> R.drawable.ic_public_24dp @@ -528,7 +532,7 @@ class ComposeActivity : BaseActivity(), Status.Visibility.UNLISTED -> R.drawable.ic_lock_open_24dp else -> R.drawable.ic_lock_open_24dp } - composeToggleVisibilityButton.setImageResource(iconRes) + binding.composeToggleVisibilityButton.setImageResource(iconRes) } private fun showComposeOptions() { @@ -544,7 +548,7 @@ class ComposeActivity : BaseActivity(), private fun onScheduleClick() { if (viewModel.scheduledAt.value == null) { - composeScheduleView.openPickDateDialog() + binding.composeScheduleView.openPickDateDialog() } else { showScheduleView() } @@ -562,7 +566,7 @@ class ComposeActivity : BaseActivity(), } private fun showEmojis() { - emojiView.adapter?.let { + 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() @@ -625,10 +629,10 @@ class ComposeActivity : BaseActivity(), val layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) layoutParams.setMargins(margin, margin, margin, marginBottom) - pollPreview.layoutParams = layoutParams + binding.pollPreview.layoutParams = layoutParams - pollPreview.setOnClickListener { - val popup = PopupMenu(this, pollPreview) + binding.pollPreview.setOnClickListener { + val popup = PopupMenu(this, binding.pollPreview) val editId = 1 val removeId = 2 popup.menu.add(0, editId, 0, R.string.edit_poll) @@ -646,7 +650,7 @@ class ComposeActivity : BaseActivity(), private fun removePoll() { viewModel.poll.value = null - pollPreview.hide() + binding.pollPreview.hide() } override fun onVisibilityChanged(visibility: Status.Visibility) { @@ -657,39 +661,39 @@ class ComposeActivity : BaseActivity(), @VisibleForTesting fun calculateTextLength(): Int { var offset = 0 - val urlSpans = composeEditField.urls + val urlSpans = binding.composeEditField.urls if (urlSpans != null) { for (span in urlSpans) { offset += max(0, span.url.length - MAXIMUM_URL_LENGTH) } } - var length = composeEditField.length() - offset + var length = binding.composeEditField.length() - offset if (viewModel.showContentWarning.value!!) { - length += composeContentWarningField.length() + length += binding.composeContentWarningField.length() } return length } private fun updateVisibleCharactersLeft() { val remainingLength = maximumTootCharacters - calculateTextLength() - composeCharactersLeftView.text = String.format(Locale.getDefault(), "%d", remainingLength) + binding.composeCharactersLeftView.text = String.format(Locale.getDefault(), "%d", remainingLength) val textColor = if (remainingLength < 0) { ContextCompat.getColor(this, R.color.tusky_red) } else { ThemeUtils.getColor(this, android.R.attr.textColorTertiary) } - composeCharactersLeftView.setTextColor(textColor) + binding.composeCharactersLeftView.setTextColor(textColor) } private fun onContentWarningChanged() { - val showWarning = composeContentWarningBar.isGone + val showWarning = binding.composeContentWarningBar.isGone viewModel.contentWarningChanged(showWarning) updateVisibleCharactersLeft() } private fun verifyScheduledTime(): Boolean { - return composeScheduleView.verifyScheduledTime(composeScheduleView.getDateTime(viewModel.scheduledAt.value)) + return binding.composeScheduleView.verifyScheduledTime(binding.composeScheduleView.getDateTime(viewModel.scheduledAt.value)) } private fun onSendClicked() { @@ -724,14 +728,14 @@ class ComposeActivity : BaseActivity(), private fun sendStatus() { enableButtons(false) - val contentText = composeEditField.text.toString() + val contentText = binding.composeEditField.text.toString() var spoilerText = "" if (viewModel.showContentWarning.value!!) { - spoilerText = composeContentWarningField.text.toString() + spoilerText = binding.composeContentWarningField.text.toString() } val characterCount = calculateTextLength() if ((characterCount <= 0 || contentText.isBlank()) && viewModel.media.value!!.isEmpty()) { - composeEditField.error = getString(R.string.error_empty) + binding.composeEditField.error = getString(R.string.error_empty) enableButtons(true) } else if (characterCount <= maximumTootCharacters) { if (viewModel.media.value!!.isNotEmpty()) { @@ -746,7 +750,7 @@ class ComposeActivity : BaseActivity(), }) } else { - composeEditField.error = getString(R.string.error_compose_character_limit) + binding.composeEditField.error = getString(R.string.error_compose_character_limit) enableButtons(true) } } @@ -757,7 +761,7 @@ class ComposeActivity : BaseActivity(), if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { initiateMediaPicking() } else { - val bar = Snackbar.make(activityCompose, R.string.error_media_upload_permission, + val bar = Snackbar.make(binding.activityCompose, R.string.error_media_upload_permission, Snackbar.LENGTH_SHORT).apply { } @@ -812,12 +816,12 @@ class ComposeActivity : BaseActivity(), } private fun enablePollButton(enable: Boolean) { - addPollTextActionTextView.isEnabled = enable + binding.addPollTextActionTextView.isEnabled = enable val textColor = ThemeUtils.getColor(this, if (enable) android.R.attr.textColorTertiary else R.attr.textColorDisabled) - addPollTextActionTextView.setTextColor(textColor) - addPollTextActionTextView.compoundDrawablesRelative[0].colorFilter = PorterDuffColorFilter(textColor, PorterDuff.Mode.SRC_IN) + binding.addPollTextActionTextView.setTextColor(textColor) + binding.addPollTextActionTextView.compoundDrawablesRelative[0].colorFilter = PorterDuffColorFilter(textColor, PorterDuff.Mode.SRC_IN) } private fun removeMediaFromQueue(item: QueuedMedia) { @@ -835,7 +839,7 @@ class ComposeActivity : BaseActivity(), val count = clipData.itemCount if (mediaCount + count > maxUploadMediaNumber) { // check if exist media + upcoming media > 4, then prob error message. - Toast.makeText(this, getString(R.string.error_upload_max_media_reached, maxUploadMediaNumber), Toast.LENGTH_SHORT).show() + Toast.makeText(this, resources.getQuantityString(R.plurals.error_upload_max_media_reached, maxUploadMediaNumber, maxUploadMediaNumber), Toast.LENGTH_SHORT).show() } else { // if not grater then 4, upload all multiple media. for (i in 0 until count) { @@ -878,19 +882,18 @@ class ComposeActivity : BaseActivity(), } private fun showContentWarning(show: Boolean) { - TransitionManager.beginDelayedTransition(composeContentWarningBar.parent as ViewGroup) + TransitionManager.beginDelayedTransition(binding.composeContentWarningBar.parent as ViewGroup) @ColorInt val color = if (show) { - composeContentWarningBar.show() - composeContentWarningField.setSelection(composeContentWarningField.text.length) - composeContentWarningField.requestFocus() + binding.composeContentWarningBar.show() + binding.composeContentWarningField.setSelection(binding.composeContentWarningField.text.length) + binding.composeContentWarningField.requestFocus() ContextCompat.getColor(this, R.color.chinwag_green) } else { - composeContentWarningBar.hide() - composeEditField.requestFocus() + binding.composeContentWarningBar.hide() + binding.composeEditField.requestFocus() ThemeUtils.getColor(this, android.R.attr.textColorTertiary) } - composeContentWarningButton.drawable.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN) - + binding.composeContentWarningButton.drawable.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN) } override fun onOptionsItemSelected(item: MenuItem): Boolean { @@ -938,8 +941,8 @@ class ComposeActivity : BaseActivity(), } private fun handleCloseButton() { - val contentText = composeEditField.text.toString() - val contentWarning = composeContentWarningField.text.toString() + val contentText = binding.composeEditField.text.toString() + val contentWarning = binding.composeContentWarningField.text.toString() if (viewModel.didChange(contentText, contentWarning)) { AlertDialog.Builder(this) .setMessage(R.string.compose_save_draft) @@ -973,8 +976,8 @@ class ComposeActivity : BaseActivity(), private fun setEmojiList(emojiList: List?) { if (emojiList != null) { - emojiView.adapter = EmojiAdapter(emojiList, this@ComposeActivity) - enableButton(composeEmojiButton, true, emojiList.isNotEmpty()) + binding.emojiView.adapter = EmojiAdapter(emojiList, this@ComposeActivity) + enableButton(binding.composeEmojiButton, true, emojiList.isNotEmpty()) } } @@ -992,9 +995,8 @@ class ComposeActivity : BaseActivity(), } } - override fun onTimeSet(view: TimePicker, hourOfDay: Int, minute: Int) { - composeScheduleView.onTimeSet(hourOfDay, minute) - viewModel.updateScheduledAt(composeScheduleView.time) + override fun onTimeSet(time: String) { + viewModel.updateScheduledAt(time) if (verifyScheduledTime()) { scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN } else { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaPreviewAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaPreviewAdapter.kt index 093e860f..a08aebc0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaPreviewAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaPreviewAdapter.kt @@ -103,7 +103,7 @@ class MediaPreviewAdapter( progressImageView.layoutParams = layoutParams progressImageView.scaleType = ImageView.ScaleType.CENTER_CROP progressImageView.setOnClickListener { - onMediaClick(adapterPosition, progressImageView) + onMediaClick(bindingAdapterPosition, progressImageView) } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollDialog.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollDialog.kt index d0f98bac..6ace77bc 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollDialog.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollDialog.kt @@ -22,9 +22,8 @@ import android.view.LayoutInflater import android.view.WindowManager import androidx.appcompat.app.AlertDialog import com.keylesspalace.tusky.R -import com.keylesspalace.tusky.adapter.AddPollOptionsAdapter +import com.keylesspalace.tusky.databinding.DialogAddPollBinding import com.keylesspalace.tusky.entity.NewPoll -import kotlinx.android.synthetic.main.dialog_add_poll.view.* fun showAddPollDialog( context: Context, @@ -34,12 +33,12 @@ fun showAddPollDialog( onUpdatePoll: (NewPoll) -> Unit ) { - val view = LayoutInflater.from(context).inflate(R.layout.dialog_add_poll, null) + val binding = DialogAddPollBinding.inflate(LayoutInflater.from(context)) val dialog = AlertDialog.Builder(context) .setIcon(R.drawable.ic_poll_24dp) .setTitle(R.string.create_poll_title) - .setView(view) + .setView(binding.root) .setNegativeButton(android.R.string.cancel, null) .setPositiveButton(android.R.string.ok, null) .create() @@ -48,7 +47,7 @@ fun showAddPollDialog( options = poll?.options?.toMutableList() ?: mutableListOf("", ""), maxOptionLength = maxOptionLength, onOptionRemoved = { valid -> - view.addChoiceButton.isEnabled = true + binding.addChoiceButton.isEnabled = true dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = valid }, onOptionChanged = { valid -> @@ -56,9 +55,9 @@ fun showAddPollDialog( } ) - view.pollChoices.adapter = adapter + binding.pollChoices.adapter = adapter - view.addChoiceButton.setOnClickListener { + binding.addChoiceButton.setOnClickListener { if (adapter.itemCount < maxOptionCount) { adapter.addChoice() } @@ -71,14 +70,14 @@ fun showAddPollDialog( it <= poll?.expiresIn ?: 0 } - view.pollDurationSpinner.setSelection(pollDurationId) + binding.pollDurationSpinner.setSelection(pollDurationId) - view.multipleChoicesCheckBox.isChecked = poll?.multiple ?: false + binding.multipleChoicesCheckBox.isChecked = poll?.multiple ?: false dialog.setOnShowListener { val button = dialog.getButton(AlertDialog.BUTTON_POSITIVE) button.setOnClickListener { - val selectedPollDurationId = view.pollDurationSpinner.selectedItemPosition + val selectedPollDurationId = binding.pollDurationSpinner.selectedItemPosition val pollDuration = context.resources .getIntArray(R.array.poll_duration_values)[selectedPollDurationId] @@ -86,7 +85,7 @@ fun showAddPollDialog( onUpdatePoll(NewPoll( options = adapter.pollOptions, expiresIn = pollDuration, - multiple = view.multipleChoicesCheckBox.isChecked + multiple = binding.multipleChoicesCheckBox.isChecked )) dialog.dismiss() @@ -97,5 +96,4 @@ fun showAddPollDialog( // make the dialog focusable so the keyboard does not stay behind it dialog.window?.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM) - } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/AddPollOptionsAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollOptionsAdapter.kt similarity index 62% rename from app/src/main/java/com/keylesspalace/tusky/adapter/AddPollOptionsAdapter.kt rename to app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollOptionsAdapter.kt index 60241992..6a0b6a87 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/AddPollOptionsAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollOptionsAdapter.kt @@ -13,17 +13,16 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky.adapter +package com.keylesspalace.tusky.components.compose.dialog import android.text.InputFilter import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.ImageButton import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.textfield.TextInputEditText -import com.google.android.material.textfield.TextInputLayout import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ItemAddPollOptionBinding +import com.keylesspalace.tusky.util.BindingHolder import com.keylesspalace.tusky.util.onTextChanged import com.keylesspalace.tusky.util.visible @@ -32,7 +31,7 @@ class AddPollOptionsAdapter( private val maxOptionLength: Int, private val onOptionRemoved: (Boolean) -> Unit, private val onOptionChanged: (Boolean) -> Unit -): RecyclerView.Adapter() { +): RecyclerView.Adapter>() { val pollOptions: List get() = options.toList() @@ -42,11 +41,12 @@ class AddPollOptionsAdapter( notifyItemInserted(options.size - 1) } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val holder = ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_add_poll_option, parent, false)) - holder.editText.filters = arrayOf(InputFilter.LengthFilter(maxOptionLength)) + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder { + val binding = ItemAddPollOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false) + val holder = BindingHolder(binding) + binding.optionEditText.filters = arrayOf(InputFilter.LengthFilter(maxOptionLength)) - holder.editText.onTextChanged { s, _, _, _ -> + binding.optionEditText.onTextChanged { s, _, _, _ -> val pos = holder.adapterPosition if(pos != RecyclerView.NO_POSITION) { options[pos] = s.toString() @@ -59,15 +59,15 @@ class AddPollOptionsAdapter( override fun getItemCount() = options.size - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - holder.editText.setText(options[position]) + override fun onBindViewHolder(holder: BindingHolder, position: Int) { + holder.binding.optionEditText.setText(options[position]) - holder.textInputLayout.hint = holder.textInputLayout.context.getString(R.string.poll_new_choice_hint, position + 1) + holder.binding.optionTextInputLayout.hint = holder.binding.root.context.getString(R.string.poll_new_choice_hint, position + 1) - holder.deleteButton.visible(position > 1, View.INVISIBLE) + holder.binding.deleteButton.visible(position > 1, View.INVISIBLE) - holder.deleteButton.setOnClickListener { - holder.editText.clearFocus() + holder.binding.deleteButton.setOnClickListener { + holder.binding.optionEditText.clearFocus() options.removeAt(holder.adapterPosition) notifyItemRemoved(holder.adapterPosition) onOptionRemoved(validateInput()) @@ -81,12 +81,4 @@ class AddPollOptionsAdapter( return true } - } - - -class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) { - val textInputLayout: TextInputLayout = itemView.findViewById(R.id.optionTextInputLayout) - val editText: TextInputEditText = itemView.findViewById(R.id.optionEditText) - val deleteButton: ImageButton = itemView.findViewById(R.id.deleteButton) -} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt index 17db6ff6..adc72cd3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt @@ -31,6 +31,7 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LiveData import at.connyduck.sparkbutton.helpers.Utils import com.bumptech.glide.Glide +import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy import com.bumptech.glide.request.target.CustomTarget import com.bumptech.glide.request.transition.Transition import com.github.chrisbanes.photoview.PhotoView @@ -63,8 +64,8 @@ fun T.makeCaptionDialog(existingDescription: String?, (imageView.layoutParams as LinearLayout.LayoutParams).setMargins(0, margin, 0, 0) val input = EditText(this) - input.hint = getString(R.string.hint_describe_for_visually_impaired, - MEDIA_DESCRIPTION_CHARACTER_LIMIT) + input.hint = resources.getQuantityString(R.plurals.hint_describe_for_visually_impaired, + MEDIA_DESCRIPTION_CHARACTER_LIMIT, MEDIA_DESCRIPTION_CHARACTER_LIMIT) dialogLayout.addView(input) (input.layoutParams as LinearLayout.LayoutParams).setMargins(margin, margin, margin, margin) input.setLines(2) @@ -97,12 +98,14 @@ fun T.makeCaptionDialog(existingDescription: String?, dialog.show() - // Load the image and manually set it into the ImageView because it doesn't have a fixed - // size. Maybe we should limit the size of CustomTarget + // Load the image and manually set it into the ImageView because it doesn't have a fixed size. Glide.with(this) .load(previewUri) - .into(object : CustomTarget() { - override fun onLoadCleared(placeholder: Drawable?) {} + .downsample(DownsampleStrategy.CENTER_INSIDE) + .into(object : CustomTarget(4096, 4096) { + override fun onLoadCleared(placeholder: Drawable?) { + imageView.setImageDrawable(placeholder) + } override fun onResourceReady(resource: Drawable, transition: Transition?) { imageView.setImageDrawable(resource) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ComposeScheduleView.java b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ComposeScheduleView.java index a1a99a7d..14c574a1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ComposeScheduleView.java +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ComposeScheduleView.java @@ -17,7 +17,6 @@ package com.keylesspalace.tusky.components.compose.view; import android.content.Context; import android.graphics.drawable.Drawable; -import android.os.Bundle; import android.util.AttributeSet; import android.widget.Button; import android.widget.TextView; @@ -31,8 +30,9 @@ import androidx.core.content.ContextCompat; import com.google.android.material.datepicker.CalendarConstraints; import com.google.android.material.datepicker.DateValidatorPointForward; import com.google.android.material.datepicker.MaterialDatePicker; +import com.google.android.material.timepicker.MaterialTimePicker; +import com.google.android.material.timepicker.TimeFormat; import com.keylesspalace.tusky.R; -import com.keylesspalace.tusky.fragment.TimePickerFragment; import java.text.DateFormat; import java.text.ParseException; @@ -44,6 +44,12 @@ import java.util.TimeZone; public class ComposeScheduleView extends ConstraintLayout { + public interface OnTimeSetListener { + void onTimeSet(String time); + } + + private OnTimeSetListener listener; + private DateFormat dateFormat; private DateFormat timeFormat; private SimpleDateFormat iso8601; @@ -92,6 +98,10 @@ public class ComposeScheduleView extends ConstraintLayout { setEditIcons(); } + public void setListener(OnTimeSetListener listener) { + this.listener = listener; + } + private void setScheduledDateTime() { if (scheduleDateTime == null) { scheduledDateTimeView.setText(""); @@ -144,13 +154,20 @@ public class ComposeScheduleView extends ConstraintLayout { } private void openPickTimeDialog() { - TimePickerFragment picker = new TimePickerFragment(); + MaterialTimePicker.Builder pickerBuilder = new MaterialTimePicker.Builder(); if (scheduleDateTime != null) { - Bundle args = new Bundle(); - args.putInt(TimePickerFragment.PICKER_TIME_HOUR, scheduleDateTime.get(Calendar.HOUR_OF_DAY)); - args.putInt(TimePickerFragment.PICKER_TIME_MINUTE, scheduleDateTime.get(Calendar.MINUTE)); - picker.setArguments(args); + pickerBuilder.setHour(scheduleDateTime.get(Calendar.HOUR_OF_DAY)) + .setMinute(scheduleDateTime.get(Calendar.MINUTE)); } + if (android.text.format.DateFormat.is24HourFormat(this.getContext())) { + pickerBuilder.setTimeFormat(TimeFormat.CLOCK_24H); + } else { + pickerBuilder.setTimeFormat(TimeFormat.CLOCK_12H); + } + + MaterialTimePicker picker = pickerBuilder.build(); + picker.addOnPositiveButtonClickListener(v -> onTimeSet(picker.getHour(), picker.getMinute())); + picker.show(((AppCompatActivity) getContext()).getSupportFragmentManager(), "time_picker"); } @@ -200,11 +217,14 @@ public class ComposeScheduleView extends ConstraintLayout { openPickTimeDialog(); } - public void onTimeSet(int hourOfDay, int minute) { + private void onTimeSet(int hourOfDay, int minute) { initializeSuggestedTime(); scheduleDateTime.set(Calendar.HOUR_OF_DAY, hourOfDay); scheduleDateTime.set(Calendar.MINUTE, minute); setScheduledDateTime(); + if (listener != null) { + listener.onTimeSet(getTime()); + } } public String getTime() { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/PollPreviewView.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/PollPreviewView.kt index 63e627fc..1126047d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/PollPreviewView.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/PollPreviewView.kt @@ -17,11 +17,12 @@ package com.keylesspalace.tusky.components.compose.view import android.content.Context import android.util.AttributeSet +import android.view.LayoutInflater import android.widget.LinearLayout import com.keylesspalace.tusky.R import com.keylesspalace.tusky.adapter.PreviewPollOptionsAdapter +import com.keylesspalace.tusky.databinding.ViewPollPreviewBinding import com.keylesspalace.tusky.entity.NewPoll -import kotlinx.android.synthetic.main.view_poll_preview.view.* class PollPreviewView @JvmOverloads constructor( context: Context?, @@ -29,11 +30,11 @@ class PollPreviewView @JvmOverloads constructor( defStyleAttr: Int = 0) : LinearLayout(context, attrs, defStyleAttr) { - val adapter = PreviewPollOptionsAdapter() + private val adapter = PreviewPollOptionsAdapter() + + private val binding = ViewPollPreviewBinding.inflate(LayoutInflater.from(context), this) init { - inflate(context, R.layout.view_poll_preview, this) - orientation = VERTICAL setBackgroundResource(R.drawable.card_frame) @@ -42,8 +43,7 @@ class PollPreviewView @JvmOverloads constructor( setPadding(padding, padding, padding, padding) - pollPreviewOptions.adapter = adapter - + binding.pollPreviewOptions.adapter = adapter } fun setPoll(poll: NewPoll){ @@ -52,13 +52,11 @@ class PollPreviewView @JvmOverloads constructor( val pollDurationId = resources.getIntArray(R.array.poll_duration_values).indexOfLast { it <= poll.expiresIn } - pollDurationPreview.text = resources.getStringArray(R.array.poll_duration_names)[pollDurationId] - + binding.pollDurationPreview.text = resources.getStringArray(R.array.poll_duration_names)[pollDurationId] } override fun setOnClickListener(l: OnClickListener?) { super.setOnClickListener(l) adapter.setOnClickListener(l) } - } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationAdapter.kt index 6d6aee48..376d3cd5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationAdapter.kt @@ -1,3 +1,18 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + package com.keylesspalace.tusky.components.conversation import android.view.LayoutInflater @@ -10,6 +25,7 @@ import androidx.recyclerview.widget.ListUpdateCallback import androidx.recyclerview.widget.RecyclerView import com.keylesspalace.tusky.R import com.keylesspalace.tusky.adapter.NetworkStateViewHolder +import com.keylesspalace.tusky.databinding.ItemNetworkStateBinding import com.keylesspalace.tusky.interfaces.StatusActionListener import com.keylesspalace.tusky.util.NetworkState import com.keylesspalace.tusky.util.StatusDisplayOptions @@ -49,11 +65,15 @@ class ConversationAdapter( } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false) return when (viewType) { - R.layout.item_network_state -> NetworkStateViewHolder(view, retryCallback) - R.layout.item_conversation -> ConversationViewHolder(view, statusDisplayOptions, - listener) + R.layout.item_network_state -> { + val binding = ItemNetworkStateBinding.inflate(LayoutInflater.from(parent.context), parent, false) + NetworkStateViewHolder(binding, retryCallback) + } + R.layout.item_conversation -> { + val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false) + ConversationViewHolder(view, statusDisplayOptions, listener) + } else -> throw IllegalArgumentException("unknown view type $viewType") } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt index 8ee1a284..e35d460d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt @@ -167,7 +167,7 @@ fun Account.toEntity() = ConversationAccountEntity( id, username, - displayName.orEmpty(), + name, avatar, emojis ?: emptyList() ) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java index e74be628..2d2f683d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java @@ -147,7 +147,7 @@ public class ConversationViewHolder extends StatusBaseViewHolder { /* input filter for TextViews have to be set before text */ if (collapsible && (expanded || TextUtils.isEmpty(spoilerText))) { contentCollapseButton.setOnClickListener(view -> { - int position = getAdapterPosition(); + int position = getBindingAdapterPosition(); if (position != RecyclerView.NO_POSITION) listener.onContentCollapsedChange(!collapsed, position); }); diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt index 621adae0..b24ff232 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt @@ -28,6 +28,7 @@ import androidx.recyclerview.widget.SimpleItemAnimator import com.keylesspalace.tusky.AccountActivity import com.keylesspalace.tusky.R import com.keylesspalace.tusky.ViewTagActivity +import com.keylesspalace.tusky.databinding.FragmentTimelineBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.fragment.SFragment @@ -38,7 +39,7 @@ import com.keylesspalace.tusky.util.CardViewMode import com.keylesspalace.tusky.util.NetworkState import com.keylesspalace.tusky.util.StatusDisplayOptions import com.keylesspalace.tusky.util.hide -import kotlinx.android.synthetic.main.fragment_timeline.* +import com.keylesspalace.tusky.util.viewBinding import javax.inject.Inject class ConversationsFragment : SFragment(), StatusActionListener, Injectable, ReselectableFragment { @@ -48,6 +49,8 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res private val viewModel: ConversationsViewModel by viewModels { viewModelFactory } + private val binding by viewBinding(FragmentTimelineBinding::bind) + private lateinit var adapter: ConversationAdapter private var layoutManager: LinearLayoutManager? = null @@ -73,14 +76,14 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res adapter = ConversationAdapter(statusDisplayOptions, this, ::onTopLoaded, viewModel::retry) - recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL)) + binding.recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL)) layoutManager = LinearLayoutManager(view.context) - recyclerView.layoutManager = layoutManager - recyclerView.adapter = adapter - (recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false + binding.recyclerView.layoutManager = layoutManager + binding.recyclerView.adapter = adapter + (binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false - progressBar.hide() - statusView.hide() + binding.progressBar.hide() + binding.statusView.hide() initSwipeToRefresh() @@ -97,16 +100,16 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res private fun initSwipeToRefresh() { viewModel.refreshState.observe(viewLifecycleOwner) { - swipeRefreshLayout.isRefreshing = it == NetworkState.LOADING + binding.swipeRefreshLayout.isRefreshing = it == NetworkState.LOADING } - swipeRefreshLayout.setOnRefreshListener { + binding.swipeRefreshLayout.setOnRefreshListener { viewModel.refresh() } - swipeRefreshLayout.setColorSchemeResources(R.color.chinwag_green) + binding.swipeRefreshLayout.setColorSchemeResources(R.color.chinwag_green) } private fun onTopLoaded() { - recyclerView.scrollToPosition(0) + binding.recyclerView.scrollToPosition(0) } override fun onReblog(reblog: Boolean, position: Int) { @@ -183,7 +186,7 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res private fun jumpToTop() { if (isAdded) { layoutManager?.scrollToPosition(0) - recyclerView.stopScroll() + binding.recyclerView.stopScroll() } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsAdapter.kt index 5dfbceac..5ba3716e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsAdapter.kt @@ -23,7 +23,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.keylesspalace.tusky.databinding.ItemDraftBinding import com.keylesspalace.tusky.db.DraftEntity -import com.keylesspalace.tusky.util.BindingViewHolder +import com.keylesspalace.tusky.util.BindingHolder import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.visible @@ -35,7 +35,7 @@ interface DraftActionListener { class DraftsAdapter( private val listener: DraftActionListener -) : PagedListAdapter>( +) : PagedListAdapter>( object : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: DraftEntity, newItem: DraftEntity): Boolean { return oldItem.id == newItem.id @@ -47,15 +47,15 @@ class DraftsAdapter( } ) { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingViewHolder { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder { val binding = ItemDraftBinding.inflate(LayoutInflater.from(parent.context), parent, false) - val viewHolder = BindingViewHolder(binding) + val viewHolder = BindingHolder(binding) binding.draftMediaPreview.layoutManager = LinearLayoutManager(binding.root.context, RecyclerView.HORIZONTAL, false) binding.draftMediaPreview.adapter = DraftMediaAdapter { - getItem(viewHolder.adapterPosition)?.let { draft -> + getItem(viewHolder.bindingAdapterPosition)?.let { draft -> listener.onOpenDraft(draft) } } @@ -63,7 +63,7 @@ class DraftsAdapter( return viewHolder } - override fun onBindViewHolder(holder: BindingViewHolder, position: Int) { + override fun onBindViewHolder(holder: BindingHolder, position: Int) { getItem(position)?.let { draft -> holder.binding.root.setOnClickListener { listener.onOpenDraft(draft) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/instancemute/InstanceListActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/InstanceListActivity.kt index ca04f9c7..7253112a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/instancemute/InstanceListActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/InstanceListActivity.kt @@ -4,10 +4,10 @@ import android.os.Bundle import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.instancemute.fragment.InstanceListFragment +import com.keylesspalace.tusky.databinding.ActivityAccountListBinding import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector import javax.inject.Inject -import kotlinx.android.synthetic.main.toolbar_basic.* class InstanceListActivity: BaseActivity(), HasAndroidInjector { @@ -16,9 +16,10 @@ class InstanceListActivity: BaseActivity(), HasAndroidInjector { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + val binding = ActivityAccountListBinding.inflate(layoutInflater) setContentView(R.layout.activity_account_list) - setSupportActionBar(toolbar) + setSupportActionBar(binding.includedToolbar.toolbar) supportActionBar?.apply { setTitle(R.string.title_domain_mutes) setDisplayHomeAsUpEnabled(true) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/instancemute/adapter/DomainMutesAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/adapter/DomainMutesAdapter.kt index 62ab7ef3..f475f394 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/instancemute/adapter/DomainMutesAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/adapter/DomainMutesAdapter.kt @@ -1,22 +1,31 @@ package com.keylesspalace.tusky.components.instancemute.adapter import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView -import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.instancemute.interfaces.InstanceActionListener -import kotlinx.android.synthetic.main.item_muted_domain.view.* +import com.keylesspalace.tusky.databinding.ItemMutedDomainBinding +import com.keylesspalace.tusky.util.BindingHolder + +class DomainMutesAdapter( + private val actionListener: InstanceActionListener +): RecyclerView.Adapter>() { -class DomainMutesAdapter(private val actionListener: InstanceActionListener): RecyclerView.Adapter() { var instances: MutableList = mutableListOf() var bottomLoading: Boolean = false - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_muted_domain, parent, false), actionListener) + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder { + val binding = ItemMutedDomainBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return BindingHolder(binding) } - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - holder.setupWithInstance(instances[position]) + override fun onBindViewHolder(holder: BindingHolder, position: Int) { + val instance = instances[position] + + holder.binding.mutedDomain.text = instance + holder.binding.mutedDomainUnmute.setOnClickListener { + actionListener.mute(false, instance, holder.bindingAdapterPosition) + } } override fun getItemCount(): Int { @@ -37,21 +46,10 @@ class DomainMutesAdapter(private val actionListener: InstanceActionListener): Re notifyItemInserted(instances.size) } - fun removeItem(position: Int) - { + fun removeItem(position: Int) { if (position >= 0 && position < instances.size) { instances.removeAt(position) notifyItemRemoved(position) } } - - - class ViewHolder(rootView: View, private val actionListener: InstanceActionListener): RecyclerView.ViewHolder(rootView) { - fun setupWithInstance(instance: String) { - itemView.muted_domain.text = instance - itemView.muted_domain_unmute.setOnClickListener { - actionListener.mute(false, instance, adapterPosition) - } - } - } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/instancemute/fragment/InstanceListFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/fragment/InstanceListFragment.kt index 093fc42d..005432d8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/instancemute/fragment/InstanceListFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/fragment/InstanceListFragment.kt @@ -2,9 +2,7 @@ package com.keylesspalace.tusky.components.instancemute.fragment import android.os.Bundle import android.util.Log -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.lifecycle.Lifecycle import androidx.recyclerview.widget.DividerItemDecoration @@ -14,16 +12,17 @@ import com.google.android.material.snackbar.Snackbar import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.instancemute.adapter.DomainMutesAdapter import com.keylesspalace.tusky.components.instancemute.interfaces.InstanceActionListener +import com.keylesspalace.tusky.databinding.FragmentInstanceListBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.HttpHeaderLink import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.view.EndlessOnScrollListener import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from import com.uber.autodispose.autoDispose import io.reactivex.android.schedulers.AndroidSchedulers -import kotlinx.android.synthetic.main.fragment_instance_list.* import retrofit2.Call import retrofit2.Callback import retrofit2.Response @@ -31,9 +30,12 @@ import java.io.IOException import javax.inject.Inject class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectable, InstanceActionListener { + @Inject lateinit var api: MastodonApi + private val binding by viewBinding(FragmentInstanceListBinding::bind) + private var fetching = false private var bottomId: String? = null private var adapter = DomainMutesAdapter(this) @@ -42,12 +44,12 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - recyclerView.setHasFixedSize(true) - recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL)) - recyclerView.adapter = adapter + binding.recyclerView.setHasFixedSize(true) + binding.recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL)) + binding.recyclerView.adapter = adapter val layoutManager = LinearLayoutManager(view.context) - recyclerView.layoutManager = layoutManager + binding.recyclerView.layoutManager = layoutManager scrollListener = object : EndlessOnScrollListener(layoutManager) { override fun onLoadMore(totalItemsCount: Int, view: RecyclerView) { @@ -57,7 +59,7 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl } } - recyclerView.addOnScrollListener(scrollListener) + binding.recyclerView.addOnScrollListener(scrollListener) fetchInstances() } @@ -85,7 +87,7 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl override fun onResponse(call: Call, response: Response) { if (response.isSuccessful) { adapter.removeItem(position) - Snackbar.make(recyclerView, getString(R.string.confirmation_domain_unmuted, instance), Snackbar.LENGTH_LONG) + Snackbar.make(binding.recyclerView, getString(R.string.confirmation_domain_unmuted, instance), Snackbar.LENGTH_LONG) .setAction(R.string.action_undo) { mute(true, instance, position) } @@ -103,10 +105,10 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl return } fetching = true - instanceProgressBar.show() + binding.instanceProgressBar.show() if (id != null) { - recyclerView.post { adapter.bottomLoading = true } + binding.recyclerView.post { adapter.bottomLoading = true } } api.domainBlocks(id, bottomId) @@ -116,7 +118,7 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl val instances = response.body() if (response.isSuccessful && instances != null) { - onFetchInstancesSuccess(instances, response.headers().get("Link")) + onFetchInstancesSuccess(instances, response.headers()["Link"]) } else { onFetchInstancesFailure(Exception(response.message())) } @@ -127,7 +129,7 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl private fun onFetchInstancesSuccess(instances: List, linkHeader: String?) { adapter.bottomLoading = false - instanceProgressBar.hide() + binding.instanceProgressBar.hide() val links = HttpHeaderLink.parse(linkHeader) val next = HttpHeaderLink.findByRelationType(links, "next") @@ -137,32 +139,32 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl fetching = false if (adapter.itemCount == 0) { - messageView.show() - messageView.setup( + binding.messageView.show() + binding.messageView.setup( R.drawable.elephant_friend_empty, R.string.message_empty, null ) } else { - messageView.hide() + binding.messageView.hide() } } private fun onFetchInstancesFailure(throwable: Throwable) { fetching = false - instanceProgressBar.hide() + binding.instanceProgressBar.hide() Log.e(TAG, "Fetch failure", throwable) if (adapter.itemCount == 0) { - messageView.show() + binding.messageView.show() if (throwable is IOException) { - messageView.setup(R.drawable.elephant_offline, R.string.error_network) { - messageView.hide() + binding.messageView.setup(R.drawable.elephant_offline, R.string.error_network) { + binding.messageView.hide() this.fetchInstances(null) } } else { - messageView.setup(R.drawable.elephant_error, R.string.error_generic) { - messageView.hide() + binding.messageView.setup(R.drawable.elephant_error, R.string.error_generic) { + binding.messageView.hide() this.fetchInstances(null) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java index 0622fd47..16499757 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java @@ -242,7 +242,7 @@ public class NotificationHelper { if (currentNotifications.length() != 1) { try { - String title = context.getString(R.string.notification_title_summary, currentNotifications.length()); + String title = context.getResources().getQuantityString(R.plurals.notification_title_summary, currentNotifications.length(), currentNotifications.length()); String text = joinNames(context, currentNotifications); summaryBuilder.setContentTitle(title) .setContentText(text); diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/EmojiPreference.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/EmojiPreference.kt index c0e538a9..2d32723f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/preference/EmojiPreference.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/EmojiPreference.kt @@ -8,14 +8,23 @@ import android.os.Build import android.util.Log import android.view.LayoutInflater import android.view.View -import android.widget.* +import android.widget.RadioButton +import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.preference.Preference import androidx.preference.PreferenceManager import com.keylesspalace.tusky.R import com.keylesspalace.tusky.SplashActivity +import com.keylesspalace.tusky.databinding.DialogEmojicompatBinding +import com.keylesspalace.tusky.databinding.ItemEmojiPrefBinding import com.keylesspalace.tusky.util.EmojiCompatFont +import com.keylesspalace.tusky.util.EmojiCompatFont.Companion.BLOBMOJI import com.keylesspalace.tusky.util.EmojiCompatFont.Companion.FONTS +import com.keylesspalace.tusky.util.EmojiCompatFont.Companion.NOTOEMOJI +import com.keylesspalace.tusky.util.EmojiCompatFont.Companion.SYSTEM_DEFAULT +import com.keylesspalace.tusky.util.EmojiCompatFont.Companion.TWEMOJI +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.show import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import okhttp3.OkHttpClient @@ -50,94 +59,85 @@ class EmojiPreference( } override fun onClick() { - val view = LayoutInflater.from(context).inflate(R.layout.dialog_emojicompat, null) - viewIds.forEachIndexed { index, viewId -> - setupItem(view.findViewById(viewId), FONTS[index]) - } + val binding = DialogEmojicompatBinding.inflate(LayoutInflater.from(context)) + + setupItem(BLOBMOJI, binding.itemBlobmoji) + setupItem(TWEMOJI, binding.itemTwemoji) + setupItem(NOTOEMOJI, binding.itemNotoemoji) + setupItem(SYSTEM_DEFAULT, binding.itemNomoji) + AlertDialog.Builder(context) - .setView(view) + .setView(binding.root) .setPositiveButton(android.R.string.ok) { _, _ -> onDialogOk() } .setNegativeButton(android.R.string.cancel, null) .show() } - private fun setupItem(container: View, font: EmojiCompatFont) { - val title: TextView = container.findViewById(R.id.emojicompat_name) - val caption: TextView = container.findViewById(R.id.emojicompat_caption) - val thumb: ImageView = container.findViewById(R.id.emojicompat_thumb) - val download: ImageButton = container.findViewById(R.id.emojicompat_download) - val cancel: ImageButton = container.findViewById(R.id.emojicompat_download_cancel) - val radio: RadioButton = container.findViewById(R.id.emojicompat_radio) - + private fun setupItem(font: EmojiCompatFont, binding: ItemEmojiPrefBinding) { // Initialize all the views - title.text = font.getDisplay(container.context) - caption.setText(font.caption) - thumb.setImageResource(font.img) + binding.emojiName.text = font.getDisplay(context) + binding.emojiCaption.setText(font.caption) + binding.emojiThumbnail.setImageResource(font.img) // There needs to be a list of all the radio buttons in order to uncheck them when one is selected - radioButtons.add(radio) - updateItem(font, container) + radioButtons.add(binding.emojiRadioButton) + updateItem(font, binding) // Set actions - download.setOnClickListener { startDownload(font, container) } - cancel.setOnClickListener { cancelDownload(font, container) } - radio.setOnClickListener { radioButton: View -> select(font, radioButton as RadioButton) } - container.setOnClickListener { containerView: View -> - select(font, containerView.findViewById(R.id.emojicompat_radio)) + binding.emojiDownload.setOnClickListener { startDownload(font, binding) } + binding.emojiDownloadCancel.setOnClickListener { cancelDownload(font, binding) } + binding.emojiRadioButton.setOnClickListener { radioButton: View -> select(font, radioButton as RadioButton) } + binding.root.setOnClickListener { + select(font, binding.emojiRadioButton) } } - private fun startDownload(font: EmojiCompatFont, container: View) { - val download: ImageButton = container.findViewById(R.id.emojicompat_download) - val caption: TextView = container.findViewById(R.id.emojicompat_caption) - val progressBar: ProgressBar = container.findViewById(R.id.emojicompat_progress) - val cancel: ImageButton = container.findViewById(R.id.emojicompat_download_cancel) - + private fun startDownload(font: EmojiCompatFont, binding: ItemEmojiPrefBinding) { // Switch to downloading style - download.visibility = View.GONE - caption.visibility = View.INVISIBLE - progressBar.visibility = View.VISIBLE - progressBar.progress = 0 - cancel.visibility = View.VISIBLE + binding.emojiDownload.hide() + binding.emojiCaption.visibility = View.INVISIBLE + binding.emojiProgress.show() + binding.emojiProgress.progress = 0 + binding.emojiDownloadCancel.show() font.downloadFontFile(context, okHttpClient) .observeOn(AndroidSchedulers.mainThread()) .subscribe( { progress -> // The progress is returned as a float between 0 and 1, or -1 if it could not determined if (progress >= 0) { - progressBar.isIndeterminate = false - val max = progressBar.max.toFloat() + binding.emojiProgress.isIndeterminate = false + val max = binding.emojiProgress.max.toFloat() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - progressBar.setProgress((max * progress).toInt(), true) + binding.emojiProgress.setProgress((max * progress).toInt(), true) } else { - progressBar.progress = (max * progress).toInt() + binding.emojiProgress.progress = (max * progress).toInt() } } else { - progressBar.isIndeterminate = true + binding.emojiProgress.isIndeterminate = true } }, { Toast.makeText(context, R.string.download_failed, Toast.LENGTH_SHORT).show() - updateItem(font, container) + updateItem(font, binding) }, { - finishDownload(font, container) + finishDownload(font, binding) } ).also { downloadDisposables[font.id] = it } } - private fun cancelDownload(font: EmojiCompatFont, container: View) { - font.deleteDownloadedFile(container.context) + private fun cancelDownload(font: EmojiCompatFont, binding: ItemEmojiPrefBinding) { + font.deleteDownloadedFile(context) downloadDisposables[font.id]?.dispose() downloadDisposables[font.id] = null - updateItem(font, container) + updateItem(font, binding) } - private fun finishDownload(font: EmojiCompatFont, container: View) { - select(font, container.findViewById(R.id.emojicompat_radio)) - updateItem(font, container) + private fun finishDownload(font: EmojiCompatFont, binding: ItemEmojiPrefBinding) { + select(font, binding.emojiRadioButton) + updateItem(font, binding) // Set the flag to restart the app (because an update has been downloaded) if (selected === original && currentNeedsUpdate) { updated = true @@ -153,54 +153,43 @@ class EmojiPreference( */ private fun select(font: EmojiCompatFont, radio: RadioButton) { selected = font - // Uncheck all the other buttons - for (other in radioButtons) { - if (other !== radio) { - other.isChecked = false - } + radioButtons.forEach { radioButton -> + radioButton.isChecked = radioButton == radio } - radio.isChecked = true } /** * Called when a "consistent" state is reached, i.e. it's not downloading the font * * @param font The font to be displayed - * @param container The ConstraintLayout containing the item + * @param binding The ItemEmojiPrefBinding to show the item in */ - private fun updateItem(font: EmojiCompatFont, container: View) { - // Assignments - val download: ImageButton = container.findViewById(R.id.emojicompat_download) - val caption: TextView = container.findViewById(R.id.emojicompat_caption) - val progress: ProgressBar = container.findViewById(R.id.emojicompat_progress) - val cancel: ImageButton = container.findViewById(R.id.emojicompat_download_cancel) - val radio: RadioButton = container.findViewById(R.id.emojicompat_radio) - + private fun updateItem(font: EmojiCompatFont, binding: ItemEmojiPrefBinding) { // There's no download going on - progress.visibility = View.GONE - cancel.visibility = View.GONE - caption.visibility = View.VISIBLE + binding.emojiProgress.hide() + binding.emojiDownloadCancel.hide() + binding.emojiCaption.show() if (font.isDownloaded(context)) { // Make it selectable - download.visibility = View.GONE - radio.visibility = View.VISIBLE - container.isClickable = true + binding.emojiDownload.hide() + binding.emojiRadioButton.show() + binding.root.isClickable = true } else { // Make it downloadable - download.visibility = View.VISIBLE - radio.visibility = View.GONE - container.isClickable = false + binding.emojiDownload.show() + binding.emojiRadioButton.hide() + binding.root.isClickable = false } // Select it if necessary if (font === selected) { - radio.isChecked = true + binding.emojiRadioButton.isChecked = true // Update available if (!font.isDownloaded(context)) { currentNeedsUpdate = true } } else { - radio.isChecked = false + binding.emojiRadioButton.isChecked = false } } @@ -246,13 +235,5 @@ class EmojiPreference( companion object { private const val TAG = "EmojiPreference" - - // Please note that this array must sorted in the same way as the fonts. - private val viewIds = intArrayOf( - R.id.item_nomoji, - R.id.item_blobmoji, - R.id.item_twemoji, - R.id.item_notoemoji - ) } } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt index 7bb8766a..f1a07615 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt @@ -28,12 +28,12 @@ import com.keylesspalace.tusky.MainActivity import com.keylesspalace.tusky.R import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.PreferenceChangedEvent +import com.keylesspalace.tusky.databinding.ActivityPreferencesBinding import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.util.ThemeUtils import com.keylesspalace.tusky.util.getNonNullString import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector -import kotlinx.android.synthetic.main.toolbar_basic.* import javax.inject.Inject class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreferenceChangeListener, @@ -48,12 +48,12 @@ class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreference private var restartActivitiesOnExit: Boolean = false override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_preferences) + val binding = ActivityPreferencesBinding.inflate(layoutInflater) + setContentView(binding.root) - setSupportActionBar(toolbar) + setSupportActionBar(binding.includedToolbar.toolbar) supportActionBar?.run { setDisplayHomeAsUpEnabled(true) setDisplayShowHomeEnabled(true) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt index 2c7f2d46..57c9214c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt @@ -22,11 +22,11 @@ import androidx.activity.viewModels import com.keylesspalace.tusky.BottomSheetActivity import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.report.adapter.ReportPagerAdapter +import com.keylesspalace.tusky.databinding.ActivityReportBinding import com.keylesspalace.tusky.di.ViewModelFactory +import com.keylesspalace.tusky.util.viewBinding import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector -import kotlinx.android.synthetic.main.activity_report.* -import kotlinx.android.synthetic.main.toolbar_basic.* import javax.inject.Inject class ReportActivity : BottomSheetActivity(), HasAndroidInjector { @@ -39,6 +39,8 @@ class ReportActivity : BottomSheetActivity(), HasAndroidInjector { private val viewModel: ReportViewModel by viewModels { viewModelFactory } + private val binding by viewBinding(ActivityReportBinding::inflate) + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val accountId = intent?.getStringExtra(ACCOUNT_ID) @@ -50,9 +52,9 @@ class ReportActivity : BottomSheetActivity(), HasAndroidInjector { viewModel.init(accountId, accountUserName, intent?.getStringExtra(STATUS_ID)) - setContentView(R.layout.activity_report) + setContentView(binding.root) - setSupportActionBar(toolbar) + setSupportActionBar(binding.includedToolbar.toolbar) supportActionBar?.apply { title = getString(R.string.report_username_format, viewModel.accountUserName) @@ -69,8 +71,8 @@ class ReportActivity : BottomSheetActivity(), HasAndroidInjector { } private fun initViewPager() { - wizard.isUserInputEnabled = false - wizard.adapter = ReportPagerAdapter(this) + binding.wizard.isUserInputEnabled = false + binding.wizard.adapter = ReportPagerAdapter(this) } private fun subscribeObservables() { @@ -96,18 +98,18 @@ class ReportActivity : BottomSheetActivity(), HasAndroidInjector { } private fun showPreviousScreen() { - when (wizard.currentItem) { + when (binding.wizard.currentItem) { 0 -> closeScreen() 1 -> showStatusesPage() } } private fun showDonePage() { - wizard.currentItem = 2 + binding.wizard.currentItem = 2 } private fun showNotesPage() { - wizard.currentItem = 1 + binding.wizard.currentItem = 1 } private fun closeScreen() { @@ -115,7 +117,7 @@ class ReportActivity : BottomSheetActivity(), HasAndroidInjector { } private fun showStatusesPage() { - wizard.currentItem = 0 + binding.wizard.currentItem = 0 } companion object { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt index 8201de2e..90579a92 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt @@ -21,6 +21,7 @@ import android.view.View import androidx.recyclerview.widget.RecyclerView import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.report.model.StatusViewState +import com.keylesspalace.tusky.databinding.ItemReportStatusBinding import com.keylesspalace.tusky.entity.Emoji import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.interfaces.LinkListener @@ -28,16 +29,15 @@ import com.keylesspalace.tusky.util.* import com.keylesspalace.tusky.util.StatusViewHelper.Companion.COLLAPSE_INPUT_FILTER import com.keylesspalace.tusky.util.StatusViewHelper.Companion.NO_INPUT_FILTER import com.keylesspalace.tusky.viewdata.toViewData -import kotlinx.android.synthetic.main.item_report_status.view.* import java.util.* class StatusViewHolder( - itemView: View, + private val binding: ItemReportStatusBinding, private val statusDisplayOptions: StatusDisplayOptions, private val viewState: StatusViewState, private val adapterHandler: AdapterHandler, private val getStatusForPosition: (Int) -> Status? -) : RecyclerView.ViewHolder(itemView) { +) : RecyclerView.ViewHolder(binding.root) { private val mediaViewHeight = itemView.context.resources.getDimensionPixelSize(R.dimen.status_media_preview_height) private val statusViewHelper = StatusViewHelper(itemView) @@ -56,16 +56,16 @@ class StatusViewHolder( } init { - itemView.statusSelection.setOnCheckedChangeListener { _, isChecked -> + binding.statusSelection.setOnCheckedChangeListener { _, isChecked -> status()?.let { status -> adapterHandler.setStatusChecked(status, isChecked) } } - itemView.status_media_preview_container.clipToOutline = true + binding.statusMediaPreviewContainer.clipToOutline = true } fun bind(status: Status) { - itemView.statusSelection.isChecked = adapterHandler.isStatusChecked(status.id) + binding.statusSelection.isChecked = adapterHandler.isStatusChecked(status.id) updateTextView() @@ -86,18 +86,18 @@ class StatusViewHolder( if (status.spoilerText.isBlank()) { setTextVisible(true, status.content, status.mentions, status.emojis, adapterHandler) - itemView.statusContentWarningButton.hide() - itemView.statusContentWarningDescription.hide() + binding.statusContentWarningButton.hide() + binding.statusContentWarningDescription.hide() } else { - val emojiSpoiler = status.spoilerText.emojify(status.emojis, itemView.statusContentWarningDescription, statusDisplayOptions.animateEmojis) - itemView.statusContentWarningDescription.text = emojiSpoiler - itemView.statusContentWarningDescription.show() - itemView.statusContentWarningButton.show() + val emojiSpoiler = status.spoilerText.emojify(status.emojis, binding.statusContentWarningDescription, statusDisplayOptions.animateEmojis) + binding.statusContentWarningDescription.text = emojiSpoiler + binding.statusContentWarningDescription.show() + binding.statusContentWarningButton.show() setContentWarningButtonText(viewState.isContentShow(status.id, true)) - itemView.statusContentWarningButton.setOnClickListener { + binding.statusContentWarningButton.setOnClickListener { status()?.let { status -> val contentShown = viewState.isContentShow(status.id, true) - itemView.statusContentWarningDescription.invalidate() + binding.statusContentWarningDescription.invalidate() viewState.setContentShow(status.id, !contentShown) setTextVisible(!contentShown, status.content, status.mentions, status.emojis, adapterHandler) setContentWarningButtonText(!contentShown) @@ -110,9 +110,9 @@ class StatusViewHolder( private fun setContentWarningButtonText(contentShown: Boolean) { if(contentShown) { - itemView.statusContentWarningButton.setText(R.string.status_content_warning_show_less) + binding.statusContentWarningButton.setText(R.string.status_content_warning_show_less) } else { - itemView.statusContentWarningButton.setText(R.string.status_content_warning_show_more) + binding.statusContentWarningButton.setText(R.string.status_content_warning_show_more) } } @@ -122,26 +122,26 @@ class StatusViewHolder( emojis: List, listener: LinkListener) { if (expanded) { - val emojifiedText = content.emojify(emojis, itemView.statusContent, statusDisplayOptions.animateEmojis) - LinkHelper.setClickableText(itemView.statusContent, emojifiedText, mentions, listener) + val emojifiedText = content.emojify(emojis, binding.statusContent, statusDisplayOptions.animateEmojis) + LinkHelper.setClickableText(binding.statusContent, emojifiedText, mentions, listener) } else { - LinkHelper.setClickableMentions(itemView.statusContent, mentions, listener) + LinkHelper.setClickableMentions(binding.statusContent, mentions, listener) } - if (itemView.statusContent.text.isNullOrBlank()) { - itemView.statusContent.hide() + if (binding.statusContent.text.isNullOrBlank()) { + binding.statusContent.hide() } else { - itemView.statusContent.show() + binding.statusContent.show() } } private fun setCreatedAt(createdAt: Date?) { if (statusDisplayOptions.useAbsoluteTime) { - itemView.timestampInfo.text = statusViewHelper.getAbsoluteTime(createdAt) + binding.timestampInfo.text = statusViewHelper.getAbsoluteTime(createdAt) } else { - itemView.timestampInfo.text = if (createdAt != null) { + binding.timestampInfo.text = if (createdAt != null) { val then = createdAt.time val now = System.currentTimeMillis() - TimestampUtils.getRelativeTimeSpanString(itemView.timestampInfo.context, then, now) + TimestampUtils.getRelativeTimeSpanString(binding.timestampInfo.context, then, now) } else { // unknown minutes~ "?m" @@ -149,30 +149,29 @@ class StatusViewHolder( } } - private fun setupCollapsedState(collapsible: Boolean, collapsed: Boolean, expanded: Boolean, spoilerText: String) { /* input filter for TextViews have to be set before text */ if (collapsible && (expanded || TextUtils.isEmpty(spoilerText))) { - itemView.buttonToggleContent.setOnClickListener{ + binding.buttonToggleContent.setOnClickListener{ status()?.let { status -> viewState.setCollapsed(status.id, !collapsed) updateTextView() } } - itemView.buttonToggleContent.show() + binding.buttonToggleContent.show() if (collapsed) { - itemView.buttonToggleContent.setText(R.string.status_content_show_more) - itemView.statusContent.filters = COLLAPSE_INPUT_FILTER + binding.buttonToggleContent.setText(R.string.status_content_show_more) + binding.statusContent.filters = COLLAPSE_INPUT_FILTER } else { - itemView.buttonToggleContent.setText(R.string.status_content_show_less) - itemView.statusContent.filters = NO_INPUT_FILTER + binding.buttonToggleContent.setText(R.string.status_content_show_less) + binding.statusContent.filters = NO_INPUT_FILTER } } else { - itemView.buttonToggleContent.hide() - itemView.statusContent.filters = NO_INPUT_FILTER + binding.buttonToggleContent.hide() + binding.statusContent.filters = NO_INPUT_FILTER } } - private fun status() = getStatusForPosition(adapterPosition) + private fun status() = getStatusForPosition(bindingAdapterPosition) } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesAdapter.kt index 34817ca0..b66ac4f3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesAdapter.kt @@ -20,8 +20,8 @@ import android.view.ViewGroup import androidx.paging.PagedListAdapter import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView -import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.report.model.StatusViewState +import com.keylesspalace.tusky.databinding.ItemReportStatusBinding import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.util.StatusDisplayOptions @@ -29,29 +29,25 @@ class StatusesAdapter( private val statusDisplayOptions: StatusDisplayOptions, private val statusViewState: StatusViewState, private val adapterHandler: AdapterHandler -) : PagedListAdapter(STATUS_COMPARATOR) { +) : PagedListAdapter(STATUS_COMPARATOR) { private val statusForPosition: (Int) -> Status? = { position: Int -> if (position != RecyclerView.NO_POSITION) getItem(position) else null } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - val view = LayoutInflater.from(parent.context) - .inflate(R.layout.item_report_status, parent, false) - return StatusViewHolder(view, statusDisplayOptions, statusViewState, adapterHandler, + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StatusViewHolder { + val binding = ItemReportStatusBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return StatusViewHolder(binding, statusDisplayOptions, statusViewState, adapterHandler, statusForPosition) } - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + override fun onBindViewHolder(holder: StatusViewHolder, position: Int) { getItem(position)?.let { status -> - (holder as? StatusViewHolder)?.bind(status) + holder.bind(status) } - } companion object { - val STATUS_COMPARATOR = object : DiffUtil.ItemCallback() { override fun areContentsTheSame(oldItem: Status, newItem: Status): Boolean = oldItem == newItem @@ -59,7 +55,5 @@ class StatusesAdapter( override fun areItemsTheSame(oldItem: Status, newItem: Status): Boolean = oldItem.id == newItem.id } - } - -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportDoneFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportDoneFragment.kt index 03bd8ef9..794cb287 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportDoneFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportDoneFragment.kt @@ -22,12 +22,13 @@ import androidx.fragment.app.activityViewModels import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.report.ReportViewModel import com.keylesspalace.tusky.components.report.Screen +import com.keylesspalace.tusky.databinding.FragmentReportDoneBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.util.Loading import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.show -import kotlinx.android.synthetic.main.fragment_report_done.* +import com.keylesspalace.tusky.util.viewBinding import javax.inject.Inject class ReportDoneFragment : Fragment(R.layout.fragment_report_done), Injectable { @@ -37,8 +38,10 @@ class ReportDoneFragment : Fragment(R.layout.fragment_report_done), Injectable { private val viewModel: ReportViewModel by activityViewModels { viewModelFactory } + private val binding by viewBinding(FragmentReportDoneBinding::bind) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - textReported.text = getString(R.string.report_sent_success, viewModel.accountUserName) + binding.textReported.text = getString(R.string.report_sent_success, viewModel.accountUserName) handleClicks() subscribeObservables() } @@ -46,14 +49,14 @@ class ReportDoneFragment : Fragment(R.layout.fragment_report_done), Injectable { private fun subscribeObservables() { viewModel.muteState.observe(viewLifecycleOwner) { if (it !is Loading) { - buttonMute.show() - progressMute.show() + binding.buttonMute.show() + binding.progressMute.show() } else { - buttonMute.hide() - progressMute.hide() + binding.buttonMute.hide() + binding.progressMute.hide() } - buttonMute.setText(when (it.data) { + binding.buttonMute.setText(when (it.data) { true -> R.string.action_unmute else -> R.string.action_mute }) @@ -61,14 +64,14 @@ class ReportDoneFragment : Fragment(R.layout.fragment_report_done), Injectable { viewModel.blockState.observe(viewLifecycleOwner) { if (it !is Loading) { - buttonBlock.show() - progressBlock.show() + binding.buttonBlock.show() + binding.progressBlock.show() } - else{ - buttonBlock.hide() - progressBlock.hide() + else { + binding.buttonBlock.hide() + binding.progressBlock.hide() } - buttonBlock.setText(when (it.data) { + binding.buttonBlock.setText(when (it.data) { true -> R.string.action_unblock else -> R.string.action_block }) @@ -77,13 +80,13 @@ class ReportDoneFragment : Fragment(R.layout.fragment_report_done), Injectable { } private fun handleClicks() { - buttonDone.setOnClickListener { + binding.buttonDone.setOnClickListener { viewModel.navigateTo(Screen.Finish) } - buttonBlock.setOnClickListener { + binding.buttonBlock.setOnClickListener { viewModel.toggleBlock() } - buttonMute.setOnClickListener { + binding.buttonMute.setOnClickListener { viewModel.toggleMute() } } @@ -91,5 +94,4 @@ class ReportDoneFragment : Fragment(R.layout.fragment_report_done), Injectable { companion object { fun newInstance() = ReportDoneFragment() } - } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt index b933b2fa..b47b586a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt @@ -24,10 +24,10 @@ import com.google.android.material.snackbar.Snackbar import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.report.ReportViewModel import com.keylesspalace.tusky.components.report.Screen +import com.keylesspalace.tusky.databinding.FragmentReportNoteBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.util.* -import kotlinx.android.synthetic.main.fragment_report_note.* import java.io.IOException import javax.inject.Inject @@ -38,6 +38,8 @@ class ReportNoteFragment : Fragment(R.layout.fragment_report_note), Injectable { private val viewModel: ReportViewModel by activityViewModels { viewModelFactory } + private val binding by viewBinding(FragmentReportNoteBinding::bind) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { fillViews() handleChanges() @@ -46,29 +48,29 @@ class ReportNoteFragment : Fragment(R.layout.fragment_report_note), Injectable { } private fun handleChanges() { - editNote.doAfterTextChanged { + binding.editNote.doAfterTextChanged { viewModel.reportNote = it?.toString() ?: "" } - checkIsNotifyRemote.setOnCheckedChangeListener { _, isChecked -> + binding.checkIsNotifyRemote.setOnCheckedChangeListener { _, isChecked -> viewModel.isRemoteNotify = isChecked } } private fun fillViews() { - editNote.setText(viewModel.reportNote) + binding.editNote.setText(viewModel.reportNote) if (viewModel.isRemoteAccount){ - checkIsNotifyRemote.show() - reportDescriptionRemoteInstance.show() + binding.checkIsNotifyRemote.show() + binding.reportDescriptionRemoteInstance.show() } else{ - checkIsNotifyRemote.hide() - reportDescriptionRemoteInstance.hide() + binding.checkIsNotifyRemote.hide() + binding.reportDescriptionRemoteInstance.hide() } if (viewModel.isRemoteAccount) - checkIsNotifyRemote.text = getString(R.string.report_remote_instance, viewModel.remoteServer) - checkIsNotifyRemote.isChecked = viewModel.isRemoteNotify + binding.checkIsNotifyRemote.text = getString(R.string.report_remote_instance, viewModel.remoteServer) + binding.checkIsNotifyRemote.isChecked = viewModel.isRemoteNotify } private fun subscribeObservables() { @@ -83,13 +85,13 @@ class ReportNoteFragment : Fragment(R.layout.fragment_report_note), Injectable { } private fun showError(error: Throwable?) { - editNote.isEnabled = true - checkIsNotifyRemote.isEnabled = true - buttonReport.isEnabled = true - buttonBack.isEnabled = true - progressBar.hide() + binding.editNote.isEnabled = true + binding.checkIsNotifyRemote.isEnabled = true + binding.buttonReport.isEnabled = true + binding.buttonBack.isEnabled = true + binding.progressBar.hide() - Snackbar.make(buttonBack, if (error is IOException) R.string.error_network else R.string.error_generic, Snackbar.LENGTH_LONG) + Snackbar.make(binding.buttonBack, if (error is IOException) R.string.error_network else R.string.error_generic, Snackbar.LENGTH_LONG) .apply { setAction(R.string.action_retry) { sendReport() @@ -103,19 +105,19 @@ class ReportNoteFragment : Fragment(R.layout.fragment_report_note), Injectable { } private fun showLoading() { - buttonReport.isEnabled = false - buttonBack.isEnabled = false - editNote.isEnabled = false - checkIsNotifyRemote.isEnabled = false - progressBar.show() + binding.buttonReport.isEnabled = false + binding.buttonBack.isEnabled = false + binding.editNote.isEnabled = false + binding.checkIsNotifyRemote.isEnabled = false + binding.progressBar.show() } private fun handleClicks() { - buttonBack.setOnClickListener { + binding.buttonBack.setOnClickListener { viewModel.navigateTo(Screen.Back) } - buttonReport.setOnClickListener { + binding.buttonReport.setOnClickListener { sendReport() } } @@ -123,5 +125,4 @@ class ReportNoteFragment : Fragment(R.layout.fragment_report_note), Injectable { companion object { fun newInstance() = ReportNoteFragment() } - } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt index 66b49b2b..25728a70 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt @@ -34,6 +34,7 @@ import com.keylesspalace.tusky.components.report.ReportViewModel import com.keylesspalace.tusky.components.report.Screen import com.keylesspalace.tusky.components.report.adapter.AdapterHandler import com.keylesspalace.tusky.components.report.adapter.StatusesAdapter +import com.keylesspalace.tusky.databinding.FragmentReportStatusesBinding import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory @@ -44,8 +45,8 @@ import com.keylesspalace.tusky.util.CardViewMode import com.keylesspalace.tusky.util.StatusDisplayOptions import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.viewdata.AttachmentViewData -import kotlinx.android.synthetic.main.fragment_report_statuses.* import javax.inject.Inject class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Injectable, AdapterHandler { @@ -58,6 +59,8 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje private val viewModel: ReportViewModel by activityViewModels { viewModelFactory } + private val binding by viewBinding(FragmentReportStatusesBinding::bind) + private lateinit var adapter: StatusesAdapter private var snackbarErrorRetry: Snackbar? = null @@ -93,9 +96,9 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje } private fun setupSwipeRefreshLayout() { - swipeRefreshLayout.setColorSchemeResources(R.color.chinwag_green) + binding.swipeRefreshLayout.setColorSchemeResources(R.color.chinwag_green) - swipeRefreshLayout.setOnRefreshListener { + binding.swipeRefreshLayout.setOnRefreshListener { snackbarErrorRetry?.dismiss() viewModel.refreshStatuses() } @@ -118,10 +121,10 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje adapter = StatusesAdapter(statusDisplayOptions, viewModel.statusViewState, this) - recyclerView.addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL)) - recyclerView.layoutManager = LinearLayoutManager(requireContext()) - recyclerView.adapter = adapter - (recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false + binding.recyclerView.addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL)) + binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) + binding.recyclerView.adapter = adapter + (binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false viewModel.statuses.observe(viewLifecycleOwner) { adapter.submitList(it) @@ -129,9 +132,9 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje viewModel.networkStateAfter.observe(viewLifecycleOwner) { if (it?.status == com.keylesspalace.tusky.util.Status.RUNNING) - progressBarBottom.show() + binding.progressBarBottom.show() else - progressBarBottom.hide() + binding.progressBarBottom.hide() if (it?.status == com.keylesspalace.tusky.util.Status.FAILED) showError(it.msg) @@ -139,22 +142,22 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje viewModel.networkStateBefore.observe(viewLifecycleOwner) { if (it?.status == com.keylesspalace.tusky.util.Status.RUNNING) - progressBarTop.show() + binding.progressBarTop.show() else - progressBarTop.hide() + binding.progressBarTop.hide() if (it?.status == com.keylesspalace.tusky.util.Status.FAILED) showError(it.msg) } viewModel.networkStateRefresh.observe(viewLifecycleOwner) { - if (it?.status == com.keylesspalace.tusky.util.Status.RUNNING && !swipeRefreshLayout.isRefreshing) - progressBarLoading.show() + if (it?.status == com.keylesspalace.tusky.util.Status.RUNNING && !binding.swipeRefreshLayout.isRefreshing) + binding.progressBarLoading.show() else - progressBarLoading.hide() + binding.progressBarLoading.hide() if (it?.status != com.keylesspalace.tusky.util.Status.RUNNING) - swipeRefreshLayout.isRefreshing = false + binding.swipeRefreshLayout.isRefreshing = false if (it?.status == com.keylesspalace.tusky.util.Status.FAILED) showError(it.msg) } @@ -162,7 +165,7 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje private fun showError(@Suppress("UNUSED_PARAMETER") msg: String?) { if (snackbarErrorRetry?.isShown != true) { - snackbarErrorRetry = Snackbar.make(swipeRefreshLayout, R.string.failed_fetch_statuses, Snackbar.LENGTH_INDEFINITE) + snackbarErrorRetry = Snackbar.make(binding.swipeRefreshLayout, R.string.failed_fetch_statuses, Snackbar.LENGTH_INDEFINITE) snackbarErrorRetry?.setAction(R.string.action_retry) { viewModel.retryStatusLoad() } @@ -172,11 +175,11 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje private fun handleClicks() { - buttonCancel.setOnClickListener { + binding.buttonCancel.setOnClickListener { viewModel.navigateTo(Screen.Back) } - buttonContinue.setOnClickListener { + binding.buttonContinue.setOnClickListener { viewModel.navigateTo(Screen.Note) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootActivity.kt index 66eb0983..d0fbb08c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootActivity.kt @@ -18,20 +18,19 @@ package com.keylesspalace.tusky.components.scheduled import android.content.Context import android.content.Intent import android.os.Bundle -import androidx.lifecycle.ViewModelProvider +import androidx.activity.viewModels import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.compose.ComposeActivity +import com.keylesspalace.tusky.databinding.ActivityScheduledTootBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.entity.ScheduledStatus import com.keylesspalace.tusky.util.Status import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.show -import kotlinx.android.synthetic.main.activity_scheduled_toot.* -import kotlinx.android.synthetic.main.toolbar_basic.* import javax.inject.Inject class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injectable { @@ -39,31 +38,31 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec @Inject lateinit var viewModelFactory: ViewModelFactory - lateinit var viewModel: ScheduledTootViewModel + private val viewModel: ScheduledTootViewModel by viewModels { viewModelFactory } private val adapter = ScheduledTootAdapter(this) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_scheduled_toot) - setSupportActionBar(toolbar) + val binding = ActivityScheduledTootBinding.inflate(layoutInflater) + setContentView(binding.root) + + setSupportActionBar(binding.includedToolbar.toolbar) supportActionBar?.run { title = getString(R.string.title_scheduled_toot) setDisplayHomeAsUpEnabled(true) setDisplayShowHomeEnabled(true) } - swipeRefreshLayout.setOnRefreshListener(this::refreshStatuses) - swipeRefreshLayout.setColorSchemeResources(R.color.chinwag_green) + binding.swipeRefreshLayout.setOnRefreshListener(this::refreshStatuses) + binding.swipeRefreshLayout.setColorSchemeResources(R.color.chinwag_green) - scheduledTootList.setHasFixedSize(true) - scheduledTootList.layoutManager = LinearLayoutManager(this) + binding.scheduledTootList.setHasFixedSize(true) + binding.scheduledTootList.layoutManager = LinearLayoutManager(this) val divider = DividerItemDecoration(this, DividerItemDecoration.VERTICAL) - scheduledTootList.addItemDecoration(divider) - scheduledTootList.adapter = adapter - - viewModel = ViewModelProvider(this, viewModelFactory)[ScheduledTootViewModel::class.java] + binding.scheduledTootList.addItemDecoration(divider) + binding.scheduledTootList.adapter = adapter viewModel.data.observe(this) { adapter.submitList(it) @@ -72,31 +71,31 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec viewModel.networkState.observe(this) { (status) -> when(status) { Status.SUCCESS -> { - progressBar.hide() - swipeRefreshLayout.isRefreshing = false + binding.progressBar.hide() + binding.swipeRefreshLayout.isRefreshing = false if(viewModel.data.value?.loadedCount == 0) { - errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_scheduled_status) - errorMessageView.show() + binding.errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_scheduled_status) + binding.errorMessageView.show() } else { - errorMessageView.hide() + binding.errorMessageView.hide() } } Status.RUNNING -> { - errorMessageView.hide() + binding.errorMessageView.hide() if(viewModel.data.value?.loadedCount ?: 0 > 0) { - swipeRefreshLayout.isRefreshing = true + binding.swipeRefreshLayout.isRefreshing = true } else { - progressBar.show() + binding.progressBar.show() } } Status.FAILED -> { if(viewModel.data.value?.loadedCount ?: 0 >= 0) { - progressBar.hide() - swipeRefreshLayout.isRefreshing = false - errorMessageView.setup(R.drawable.elephant_error, R.string.error_generic) { + binding.progressBar.hide() + binding.swipeRefreshLayout.isRefreshing = false + binding.errorMessageView.setup(R.drawable.elephant_error, R.string.error_generic) { refreshStatuses() } - errorMessageView.show() + binding.errorMessageView.show() } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootAdapter.kt index ea12d1ff..414130dd 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootAdapter.kt @@ -18,13 +18,11 @@ package com.keylesspalace.tusky.components.scheduled import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.ImageButton -import android.widget.TextView import androidx.paging.PagedListAdapter import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.RecyclerView -import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ItemScheduledTootBinding import com.keylesspalace.tusky.entity.ScheduledStatus +import com.keylesspalace.tusky.util.BindingHolder interface ScheduledTootActionListener { fun edit(item: ScheduledStatus) @@ -33,7 +31,7 @@ interface ScheduledTootActionListener { class ScheduledTootAdapter( val listener: ScheduledTootActionListener -) : PagedListAdapter( +) : PagedListAdapter>( object: DiffUtil.ItemCallback(){ override fun areItemsTheSame(oldItem: ScheduledStatus, newItem: ScheduledStatus): Boolean { return oldItem.id == newItem.id @@ -46,40 +44,24 @@ class ScheduledTootAdapter( } ) { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TootViewHolder { - val view = LayoutInflater.from(parent.context) - .inflate(R.layout.item_scheduled_toot, parent, false) - return TootViewHolder(view) + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder { + val binding = ItemScheduledTootBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return BindingHolder(binding) } - override fun onBindViewHolder(viewHolder: TootViewHolder, position: Int) { - getItem(position)?.let{ - viewHolder.bind(it) - } - } - - - inner class TootViewHolder(view: View) : RecyclerView.ViewHolder(view) { - - private val text: TextView = view.findViewById(R.id.text) - private val edit: ImageButton = view.findViewById(R.id.edit) - private val delete: ImageButton = view.findViewById(R.id.delete) - - fun bind(item: ScheduledStatus) { - edit.isEnabled = true - delete.isEnabled = true - text.text = item.params.text - edit.setOnClickListener { v: View -> + override fun onBindViewHolder(holder: BindingHolder, position: Int) { + getItem(position)?.let{ item -> + holder.binding.edit.isEnabled = true + holder.binding.delete.isEnabled = true + holder.binding.text.text = item.params.text + holder.binding.edit.setOnClickListener { v: View -> v.isEnabled = false listener.edit(item) } - delete.setOnClickListener { v: View -> + holder.binding.delete.setOnClickListener { v: View -> v.isEnabled = false listener.delete(item) } - } - } - -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchActivity.kt index be705637..7208b038 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchActivity.kt @@ -26,10 +26,11 @@ import com.google.android.material.tabs.TabLayoutMediator import com.keylesspalace.tusky.BottomSheetActivity import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.search.adapter.SearchPagerAdapter +import com.keylesspalace.tusky.databinding.ActivitySearchBinding import com.keylesspalace.tusky.di.ViewModelFactory +import com.keylesspalace.tusky.util.viewBinding import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector -import kotlinx.android.synthetic.main.activity_search.* import javax.inject.Inject class SearchActivity : BottomSheetActivity(), HasAndroidInjector { @@ -41,10 +42,12 @@ class SearchActivity : BottomSheetActivity(), HasAndroidInjector { private val viewModel: SearchViewModel by viewModels { viewModelFactory } + private val binding by viewBinding(ActivitySearchBinding::inflate) + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_search) - setSupportActionBar(toolbar) + setContentView(binding.root) + setSupportActionBar(binding.toolbar) supportActionBar?.apply { setDisplayHomeAsUpEnabled(true) setDisplayShowHomeEnabled(true) @@ -55,9 +58,9 @@ class SearchActivity : BottomSheetActivity(), HasAndroidInjector { } private fun setupPages() { - pages.adapter = SearchPagerAdapter(this) + binding.pages.adapter = SearchPagerAdapter(this) - TabLayoutMediator(tabs, pages) { + TabLayoutMediator(binding.tabs, binding.pages) { tab, position -> tab.text = getPageTitle(position) }.attach() diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt index 08afe44d..57c78ef8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt @@ -193,7 +193,7 @@ class SearchViewModel @Inject constructor( return accountManager.getAllAccountsOrderedByActive() } - fun muteAccount(accountId: String, notifications: Boolean, duration: Int) { + fun muteAccount(accountId: String, notifications: Boolean, duration: Int?) { timelineCases.mute(accountId, notifications, duration) } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchHashtagsAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchHashtagsAdapter.kt index 71863d43..ebc02160 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchHashtagsAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchHashtagsAdapter.kt @@ -19,24 +19,23 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.paging.PagedListAdapter import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.RecyclerView -import com.keylesspalace.tusky.R -import com.keylesspalace.tusky.adapter.HashtagViewHolder +import com.keylesspalace.tusky.databinding.ItemHashtagBinding import com.keylesspalace.tusky.entity.HashTag import com.keylesspalace.tusky.interfaces.LinkListener +import com.keylesspalace.tusky.util.BindingHolder class SearchHashtagsAdapter(private val linkListener: LinkListener) - : PagedListAdapter(HASHTAG_COMPARATOR) { + : PagedListAdapter>(HASHTAG_COMPARATOR) { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - val view = LayoutInflater.from(parent.context) - .inflate(R.layout.item_hashtag, parent, false) - return HashtagViewHolder(view) + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder { + val binding = ItemHashtagBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return BindingHolder(binding) } - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + override fun onBindViewHolder(holder: BindingHolder, position: Int) { getItem(position)?.let { (name) -> - (holder as HashtagViewHolder).setup(name, linkListener) + holder.binding.root.text = String.format("#%s", name) + holder.binding.root.setOnClickListener { linkListener.onViewTag(name) } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchAccountsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchAccountsFragment.kt index c453f97c..8715e1ab 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchAccountsFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchAccountsFragment.kt @@ -23,11 +23,10 @@ import com.keylesspalace.tusky.components.search.adapter.SearchAccountsAdapter import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.util.NetworkState -import kotlinx.android.synthetic.main.fragment_search.* class SearchAccountsFragment : SearchFragment() { override fun createAdapter(): PagedListAdapter { - val preferences = PreferenceManager.getDefaultSharedPreferences(searchRecyclerView.context) + val preferences = PreferenceManager.getDefaultSharedPreferences(binding.searchRecyclerView.context) return SearchAccountsAdapter( this, @@ -46,5 +45,4 @@ class SearchAccountsFragment : SearchFragment() { companion object { fun newInstance() = SearchAccountsFragment() } - } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt index 7985d5ba..a1ca40c7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt @@ -17,11 +17,11 @@ import com.keylesspalace.tusky.BottomSheetActivity import com.keylesspalace.tusky.R import com.keylesspalace.tusky.ViewTagActivity import com.keylesspalace.tusky.components.search.SearchViewModel +import com.keylesspalace.tusky.databinding.FragmentSearchBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.interfaces.LinkListener import com.keylesspalace.tusky.util.* -import kotlinx.android.synthetic.main.fragment_search.* import javax.inject.Inject abstract class SearchFragment : Fragment(R.layout.fragment_search), @@ -32,6 +32,8 @@ abstract class SearchFragment : Fragment(R.layout.fragment_search), protected val viewModel: SearchViewModel by activityViewModels { viewModelFactory } + protected val binding by viewBinding(FragmentSearchBinding::bind) + private var snackbarErrorRetry: Snackbar? = null abstract fun createAdapter(): PagedListAdapter @@ -48,8 +50,8 @@ abstract class SearchFragment : Fragment(R.layout.fragment_search), } private fun setupSwipeRefreshLayout() { - swipeRefreshLayout.setOnRefreshListener(this) - swipeRefreshLayout.setColorSchemeResources(R.color.chinwag_green) + binding.swipeRefreshLayout.setOnRefreshListener(this) + binding.swipeRefreshLayout.setColorSchemeResources(R.color.chinwag_green) } private fun subscribeObservables() { @@ -59,7 +61,7 @@ abstract class SearchFragment : Fragment(R.layout.fragment_search), networkStateRefresh.observe(viewLifecycleOwner) { - searchProgressBar.visible(it == NetworkState.LOADING) + binding.searchProgressBar.visible(it == NetworkState.LOADING) if (it.status == Status.FAILED) { showError() @@ -69,7 +71,7 @@ abstract class SearchFragment : Fragment(R.layout.fragment_search), networkState.observe(viewLifecycleOwner) { - progressBarBottom.visible(it == NetworkState.LOADING) + binding.progressBarBottom.visible(it == NetworkState.LOADING) if (it.status == Status.FAILED) { showError() @@ -82,24 +84,25 @@ abstract class SearchFragment : Fragment(R.layout.fragment_search), } private fun initAdapter() { - searchRecyclerView.addItemDecoration(DividerItemDecoration(searchRecyclerView.context, DividerItemDecoration.VERTICAL)) - searchRecyclerView.layoutManager = LinearLayoutManager(searchRecyclerView.context) + binding.searchRecyclerView.addItemDecoration(DividerItemDecoration(binding.searchRecyclerView.context, DividerItemDecoration.VERTICAL)) + binding.searchRecyclerView.layoutManager = LinearLayoutManager(binding.searchRecyclerView.context) adapter = createAdapter() - searchRecyclerView.adapter = adapter - searchRecyclerView.setHasFixedSize(true) - (searchRecyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false + binding.searchRecyclerView.adapter = adapter + binding.searchRecyclerView.setHasFixedSize(true) + (binding.searchRecyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false } private fun showNoData(isEmpty: Boolean) { - if (isEmpty && networkStateRefresh.value == NetworkState.LOADED) - searchNoResultsText.show() - else - searchNoResultsText.hide() + if (isEmpty && networkStateRefresh.value == NetworkState.LOADED) { + binding.searchNoResultsText.show() + } else { + binding.searchNoResultsText.hide() + } } private fun showError() { if (snackbarErrorRetry?.isShown != true) { - snackbarErrorRetry = Snackbar.make(layoutRoot, R.string.failed_search, Snackbar.LENGTH_INDEFINITE) + snackbarErrorRetry = Snackbar.make(binding.root, R.string.failed_search, Snackbar.LENGTH_INDEFINITE) snackbarErrorRetry?.setAction(R.string.action_retry) { snackbarErrorRetry = null viewModel.retryAllSearches() @@ -122,8 +125,8 @@ abstract class SearchFragment : Fragment(R.layout.fragment_search), override fun onRefresh() { // Dismissed here because the RecyclerView bottomProgressBar is shown as soon as the retry begins. - swipeRefreshLayout.post { - swipeRefreshLayout.isRefreshing = false + binding.swipeRefreshLayout.post { + binding.swipeRefreshLayout.isRefreshing = false } viewModel.retryAllSearches() } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt index 6d96bb5a..f2ea85c0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt @@ -63,7 +63,6 @@ import com.keylesspalace.tusky.viewdata.StatusViewData import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from import com.uber.autodispose.autoDispose import io.reactivex.android.schedulers.AndroidSchedulers -import kotlinx.android.synthetic.main.fragment_search.* class SearchStatusesFragment : SearchFragment>(), StatusActionListener { @@ -78,7 +77,7 @@ class SearchStatusesFragment : SearchFragment, *> { - val preferences = PreferenceManager.getDefaultSharedPreferences(searchRecyclerView.context) + val preferences = PreferenceManager.getDefaultSharedPreferences(binding.searchRecyclerView.context) val statusDisplayOptions = StatusDisplayOptions( animateAvatars = preferences.getBoolean("animateGifAvatars", false), mediaPreviewEnabled = viewModel.mediaPreviewEnabled, @@ -91,12 +90,11 @@ class SearchStatusesFragment : SearchFragment? { diff --git a/app/src/main/java/com/keylesspalace/tusky/db/DraftEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/DraftEntity.kt index be1eca58..184ff2c3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/DraftEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/DraftEntity.kt @@ -23,7 +23,7 @@ import androidx.room.PrimaryKey import androidx.room.TypeConverters import com.keylesspalace.tusky.entity.NewPoll import com.keylesspalace.tusky.entity.Status -import kotlinx.android.parcel.Parcelize +import kotlinx.parcelize.Parcelize @Entity @TypeConverters(Converters::class) diff --git a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt index 1137b12b..13266851 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt @@ -28,6 +28,7 @@ import com.keylesspalace.tusky.appstore.EventHubImpl import com.keylesspalace.tusky.components.notifications.Notifier import com.keylesspalace.tusky.components.notifications.SystemNotifier import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.db.Converters import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.TimelineCases import com.keylesspalace.tusky.network.TimelineCasesImpl @@ -70,8 +71,9 @@ class AppModule { @Provides @Singleton - fun providesDatabase(appContext: Context): AppDatabase { + fun providesDatabase(appContext: Context, converters: Converters): AppDatabase { return Room.databaseBuilder(appContext, AppDatabase::class.java, "tuskyDB") + .addTypeConverter(converters) .allowMainThreadQueries() .addMigrations(AppDatabase.MIGRATION_2_3, AppDatabase.MIGRATION_3_4, AppDatabase.MIGRATION_4_5, AppDatabase.MIGRATION_5_6, AppDatabase.MIGRATION_6_7, AppDatabase.MIGRATION_7_8, diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Attachment.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Attachment.kt index 1de7bd78..3e14519a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Attachment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Attachment.kt @@ -22,7 +22,7 @@ import com.google.gson.JsonElement import com.google.gson.JsonParseException import com.google.gson.annotations.JsonAdapter import com.google.gson.annotations.SerializedName -import kotlinx.android.parcel.Parcelize +import kotlinx.parcelize.Parcelize @Parcelize data class Attachment( diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Emoji.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Emoji.kt index fe7a22c7..42bb99e9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Emoji.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Emoji.kt @@ -17,7 +17,7 @@ package com.keylesspalace.tusky.entity import android.os.Parcelable import com.google.gson.annotations.SerializedName -import kotlinx.android.parcel.Parcelize +import kotlinx.parcelize.Parcelize @Parcelize data class Emoji( diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/NewStatus.kt b/app/src/main/java/com/keylesspalace/tusky/entity/NewStatus.kt index ebc979f3..16cbc6a7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/NewStatus.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/NewStatus.kt @@ -17,7 +17,7 @@ package com.keylesspalace.tusky.entity import android.os.Parcelable import com.google.gson.annotations.SerializedName -import kotlinx.android.parcel.Parcelize +import kotlinx.parcelize.Parcelize data class NewStatus( val status: String, diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt index 7b202713..cf7050b8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt @@ -21,6 +21,7 @@ import android.view.View import androidx.fragment.app.Fragment import androidx.lifecycle.Lifecycle import androidx.preference.PreferenceManager +import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -31,6 +32,8 @@ import com.keylesspalace.tusky.AccountListActivity.Type import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.R import com.keylesspalace.tusky.adapter.* +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.databinding.FragmentAccountListBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Relationship @@ -40,12 +43,12 @@ import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.util.HttpHeaderLink import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.view.EndlessOnScrollListener import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from import com.uber.autodispose.autoDispose import io.reactivex.Single import io.reactivex.android.schedulers.AndroidSchedulers -import kotlinx.android.synthetic.main.fragment_account_list.* import retrofit2.Response import java.io.IOException import java.util.* @@ -55,6 +58,10 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct @Inject lateinit var api: MastodonApi + @Inject + lateinit var accountManager: AccountManager + + private val binding by viewBinding(FragmentAccountListBinding::bind) private lateinit var type: Type private var id: String? = null @@ -73,12 +80,12 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - recyclerView.setHasFixedSize(true) + binding.recyclerView.setHasFixedSize(true) val layoutManager = LinearLayoutManager(view.context) - recyclerView.layoutManager = layoutManager - (recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false + binding.recyclerView.layoutManager = layoutManager + (binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false - recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL)) + binding.recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL)) val pm = PreferenceManager.getDefaultSharedPreferences(view.context) val animateAvatar = pm.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false) @@ -87,10 +94,17 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct adapter = when (type) { Type.BLOCKS -> BlocksAdapter(this, animateAvatar, animateEmojis) Type.MUTES -> MutesAdapter(this, animateAvatar, animateEmojis) - Type.FOLLOW_REQUESTS -> FollowRequestsAdapter(this, animateAvatar, animateEmojis) + Type.FOLLOW_REQUESTS -> { + val headerAdapter = FollowRequestsHeaderAdapter(accountManager.activeAccount!!.domain, arguments?.get(ARG_ACCOUNT_LOCKED) == true) + val followRequestsAdapter = FollowRequestsAdapter(this, animateAvatar, animateEmojis) + binding.recyclerView.adapter = ConcatAdapter(headerAdapter, followRequestsAdapter) + followRequestsAdapter + } else -> FollowAdapter(this, animateAvatar, animateEmojis) } - recyclerView.adapter = adapter + if (binding.recyclerView.adapter == null) { + binding.recyclerView.adapter = adapter + } scrollListener = object : EndlessOnScrollListener(layoutManager) { override fun onLoadMore(totalItemsCount: Int, view: RecyclerView) { @@ -101,7 +115,7 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct } } - recyclerView.addOnScrollListener(scrollListener) + binding.recyclerView.addOnScrollListener(scrollListener) fetchAccounts() } @@ -136,7 +150,7 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct val unmutedUser = mutesAdapter.removeItem(position) if (unmutedUser != null) { - Snackbar.make(recyclerView, R.string.confirmation_unmuted, Snackbar.LENGTH_LONG) + Snackbar.make(binding.recyclerView, R.string.confirmation_unmuted, Snackbar.LENGTH_LONG) .setAction(R.string.action_undo) { mutesAdapter.addItem(unmutedUser, position) onMute(true, id, position, notifications) @@ -180,7 +194,7 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct val unblockedUser = blocksAdapter.removeItem(position) if (unblockedUser != null) { - Snackbar.make(recyclerView, R.string.confirmation_unblocked, Snackbar.LENGTH_LONG) + Snackbar.make(binding.recyclerView, R.string.confirmation_unblocked, Snackbar.LENGTH_LONG) .setAction(R.string.action_undo) { blocksAdapter.addItem(unblockedUser, position) onBlock(true, id, position) @@ -260,7 +274,7 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct fetching = true if (fromId != null) { - recyclerView.post { adapter.setBottomLoading(true) } + binding.recyclerView.post { adapter.setBottomLoading(true) } } getFetchCallByListType(fromId) @@ -303,14 +317,14 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct fetching = false if (adapter.itemCount == 0) { - messageView.show() - messageView.setup( + binding.messageView.show() + binding.messageView.setup( R.drawable.elephant_friend_empty, R.string.message_empty, null ) } else { - messageView.hide() + binding.messageView.hide() } } @@ -339,15 +353,15 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct Log.e(TAG, "Fetch failure", throwable) if (adapter.itemCount == 0) { - messageView.show() + binding.messageView.show() if (throwable is IOException) { - messageView.setup(R.drawable.elephant_offline, R.string.error_network) { - messageView.hide() + binding.messageView.setup(R.drawable.elephant_offline, R.string.error_network) { + binding.messageView.hide() this.fetchAccounts(null) } } else { - messageView.setup(R.drawable.elephant_error, R.string.error_generic) { - messageView.hide() + binding.messageView.setup(R.drawable.elephant_error, R.string.error_generic) { + binding.messageView.hide() this.fetchAccounts(null) } } @@ -358,15 +372,16 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct private const val TAG = "AccountList" // logging tag private const val ARG_TYPE = "type" private const val ARG_ID = "id" + private const val ARG_ACCOUNT_LOCKED = "acc_locked" - fun newInstance(type: Type, id: String? = null): AccountListFragment { + fun newInstance(type: Type, id: String? = null, accountLocked: Boolean = false): AccountListFragment { return AccountListFragment().apply { arguments = Bundle(2).apply { putSerializable(ARG_TYPE, type) putString(ARG_ID, id) + putBoolean(ARG_ACCOUNT_LOCKED, accountLocked) } } } } - } diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt index d64ffdfc..54c455b4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt @@ -30,6 +30,7 @@ import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide import com.keylesspalace.tusky.R import com.keylesspalace.tusky.ViewMediaActivity +import com.keylesspalace.tusky.databinding.FragmentTimelineBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Status @@ -39,13 +40,13 @@ import com.keylesspalace.tusky.util.LinkHelper import com.keylesspalace.tusky.util.ThemeUtils import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.view.SquareImageView import com.keylesspalace.tusky.viewdata.AttachmentViewData import com.uber.autodispose.android.lifecycle.autoDispose import io.reactivex.SingleObserver import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable -import kotlinx.android.synthetic.main.fragment_timeline.* import retrofit2.Response import java.io.IOException import java.util.* @@ -58,49 +59,36 @@ import javax.inject.Inject */ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFragment, Injectable { - companion object { - @JvmStatic - fun newInstance(accountId: String, enableSwipeToRefresh:Boolean=true): AccountMediaFragment { - val fragment = AccountMediaFragment() - val args = Bundle() - args.putString(ACCOUNT_ID_ARG, accountId) - args.putBoolean(ARG_ENABLE_SWIPE_TO_REFRESH,enableSwipeToRefresh) - fragment.arguments = args - return fragment - } - - private const val ACCOUNT_ID_ARG = "account_id" - private const val TAG = "AccountMediaFragment" - private const val ARG_ENABLE_SWIPE_TO_REFRESH = "arg.enable.swipe.to.refresh" - } - - private var isSwipeToRefreshEnabled: Boolean = true - private var needToRefresh = false @Inject lateinit var api: MastodonApi + private val binding by viewBinding(FragmentTimelineBinding::bind) + + private lateinit var accountId: String + private val adapter = MediaGridAdapter() private val statuses = mutableListOf() private var fetchingStatus = FetchingStatus.NOT_FETCHING - private lateinit var accountId: String + private var isSwipeToRefreshEnabled: Boolean = true + private var needToRefresh = false private val callback = object : SingleObserver>> { override fun onError(t: Throwable) { fetchingStatus = FetchingStatus.NOT_FETCHING if (isAdded) { - swipeRefreshLayout.isRefreshing = false - progressBar.visibility = View.GONE - topProgressBar?.hide() - statusView.show() + binding.swipeRefreshLayout.isRefreshing = false + binding.progressBar.visibility = View.GONE + binding.topProgressBar.hide() + binding.statusView.show() if (t is IOException) { - statusView.setup(R.drawable.elephant_offline, R.string.error_network) { + binding.statusView.setup(R.drawable.elephant_offline, R.string.error_network) { doInitialLoadingIfNeeded() } } else { - statusView.setup(R.drawable.elephant_error, R.string.error_generic) { + binding.statusView.setup(R.drawable.elephant_error, R.string.error_generic) { doInitialLoadingIfNeeded() } } @@ -112,9 +100,9 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr override fun onSuccess(response: Response>) { fetchingStatus = FetchingStatus.NOT_FETCHING if (isAdded) { - swipeRefreshLayout.isRefreshing = false - progressBar.visibility = View.GONE - topProgressBar?.hide() + binding.swipeRefreshLayout.isRefreshing = false + binding.progressBar.visibility = View.GONE + binding.topProgressBar.hide() val body = response.body() body?.let { fetched -> @@ -126,11 +114,11 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr } adapter.addTop(result) if (result.isNotEmpty()) - recyclerView.scrollToPosition(0) + binding.recyclerView.scrollToPosition(0) if (statuses.isEmpty()) { - statusView.show() - statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty) + binding.statusView.show() + binding.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty) } } } @@ -181,18 +169,18 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr adapter.baseItemColor = ThemeUtils.getColor(view.context, android.R.attr.windowBackground) - recyclerView.layoutManager = layoutManager - recyclerView.adapter = adapter + binding.recyclerView.layoutManager = layoutManager + binding.recyclerView.adapter = adapter if (isSwipeToRefreshEnabled) { - swipeRefreshLayout.setOnRefreshListener { + binding.swipeRefreshLayout.setOnRefreshListener { refresh() } - swipeRefreshLayout.setColorSchemeResources(R.color.chinwag_green) + binding.swipeRefreshLayout.setColorSchemeResources(R.color.chinwag_green) } - statusView.visibility = View.GONE + binding.statusView.visibility = View.GONE - recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { + binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recycler_view: RecyclerView, dx: Int, dy: Int) { if (dy > 0) { @@ -216,7 +204,7 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr } private fun refresh() { - statusView.hide() + binding.statusView.hide() if (fetchingStatus != FetchingStatus.NOT_FETCHING) return if (statuses.isEmpty()) { fetchingStatus = FetchingStatus.INITIAL_FETCHING @@ -229,12 +217,12 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr .subscribe(callback) if (!isSwipeToRefreshEnabled) - topProgressBar?.show() + binding.topProgressBar.show() } private fun doInitialLoadingIfNeeded() { if (isAdded) { - statusView.hide() + binding.statusView.hide() } if (fetchingStatus == FetchingStatus.NOT_FETCHING && statuses.isEmpty()) { fetchingStatus = FetchingStatus.INITIAL_FETCHING @@ -332,7 +320,7 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr // saving some allocations override fun onClick(v: View?) { - viewMedia(items, adapterPosition, imageView) + viewMedia(items, bindingAdapterPosition, imageView) } } } @@ -344,4 +332,19 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr needToRefresh = true } + companion object { + @JvmStatic + fun newInstance(accountId: String, enableSwipeToRefresh:Boolean=true): AccountMediaFragment { + val fragment = AccountMediaFragment() + val args = Bundle() + args.putString(ACCOUNT_ID_ARG, accountId) + args.putBoolean(ARG_ENABLE_SWIPE_TO_REFRESH,enableSwipeToRefresh) + fragment.arguments = args + return fragment + } + + private const val ACCOUNT_ID_ARG = "account_id" + private const val TAG = "AccountMediaFragment" + private const val ARG_ENABLE_SWIPE_TO_REFRESH = "arg.enable.swipe.to.refresh" + } } \ No newline at end of file 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 6ee66f42..ecc8172d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java @@ -863,6 +863,7 @@ public class NotificationsFragment extends SFragment implements adapter.setMediaPreviewEnabled(enabled); fullyRefresh(); } + break; } case "showNotificationsFilter": { if (isAdded()) { @@ -870,6 +871,7 @@ public class NotificationsFragment extends SFragment implements updateFilterVisibility(); fullyRefreshWithProgressBar(true); } + break; } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java index 9d4b45b3..ef1074a3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java @@ -511,7 +511,7 @@ public abstract class SFragment extends Fragment implements Injectable { }); } - @VisibleForTesting + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) public void reloadFilters(boolean forceRefresh) { if (filters != null && !forceRefresh) { applyFilters(forceRefresh); @@ -547,7 +547,7 @@ public abstract class SFragment extends Fragment implements Injectable { // Override to refresh your fragment } - @VisibleForTesting + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) public boolean shouldFilterStatus(Status status) { if (filterRemoveRegex && status.getPoll() != null) { diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/TimePickerFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/TimePickerFragment.java deleted file mode 100644 index 1349a59c..00000000 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/TimePickerFragment.java +++ /dev/null @@ -1,53 +0,0 @@ -/* Copyright 2019 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.fragment; - -import android.app.Dialog; -import android.app.TimePickerDialog; -import android.os.Bundle; - -import androidx.annotation.NonNull; -import androidx.fragment.app.DialogFragment; - -import com.keylesspalace.tusky.components.compose.ComposeActivity; - -import java.util.Calendar; -import java.util.TimeZone; - -public class TimePickerFragment extends DialogFragment { - - public static final String PICKER_TIME_HOUR = "picker_time_hour"; - public static final String PICKER_TIME_MINUTE = "picker_time_minute"; - - @Override - @NonNull - public Dialog onCreateDialog(Bundle savedInstanceState) { - Bundle args = getArguments(); - Calendar calendar = Calendar.getInstance(TimeZone.getDefault()); - if (args != null) { - calendar.set(Calendar.HOUR_OF_DAY, args.getInt(PICKER_TIME_HOUR)); - calendar.set(Calendar.MINUTE, args.getInt(PICKER_TIME_MINUTE)); - } - - return new TimePickerDialog(getContext(), - android.R.style.Theme_DeviceDefault_Dialog, - (ComposeActivity) getActivity(), - calendar.get(Calendar.HOUR_OF_DAY), - calendar.get(Calendar.MINUTE), - true); - } - -} diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java deleted file mode 100644 index 4f6a2e1b..00000000 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java +++ /dev/null @@ -1,1526 +0,0 @@ -/* Copyright 2017 Andrew Dawson - * - * This file is a part of Tusky. - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 3 of the - * License, or (at your option) any later version. - * - * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Tusky; if not, - * see . */ - -package com.keylesspalace.tusky.fragment; - -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.net.Uri; -import android.os.Bundle; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.accessibility.AccessibilityManager; -import android.widget.ProgressBar; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.arch.core.util.Function; -import androidx.core.content.ContextCompat; -import androidx.core.util.Pair; -import androidx.core.widget.ContentLoadingProgressBar; -import androidx.lifecycle.Lifecycle; -import androidx.preference.PreferenceManager; -import androidx.recyclerview.widget.AsyncDifferConfig; -import androidx.recyclerview.widget.AsyncListDiffer; -import androidx.recyclerview.widget.DiffUtil; -import androidx.recyclerview.widget.DividerItemDecoration; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.ListUpdateCallback; -import androidx.recyclerview.widget.RecyclerView; -import androidx.recyclerview.widget.SimpleItemAnimator; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; - -import com.google.android.material.floatingactionbutton.FloatingActionButton; -import com.keylesspalace.tusky.AccountListActivity; -import com.keylesspalace.tusky.BaseActivity; -import com.keylesspalace.tusky.R; -import com.keylesspalace.tusky.adapter.StatusBaseViewHolder; -import com.keylesspalace.tusky.adapter.TimelineAdapter; -import com.keylesspalace.tusky.appstore.BlockEvent; -import com.keylesspalace.tusky.appstore.BookmarkEvent; -import com.keylesspalace.tusky.appstore.DomainMuteEvent; -import com.keylesspalace.tusky.appstore.EventHub; -import com.keylesspalace.tusky.appstore.FavoriteEvent; -import com.keylesspalace.tusky.appstore.MuteConversationEvent; -import com.keylesspalace.tusky.appstore.MuteEvent; -import com.keylesspalace.tusky.appstore.PreferenceChangedEvent; -import com.keylesspalace.tusky.appstore.ReblogEvent; -import com.keylesspalace.tusky.appstore.StatusComposedEvent; -import com.keylesspalace.tusky.appstore.StatusDeletedEvent; -import com.keylesspalace.tusky.appstore.UnfollowEvent; -import com.keylesspalace.tusky.db.AccountManager; -import com.keylesspalace.tusky.di.Injectable; -import com.keylesspalace.tusky.entity.Filter; -import com.keylesspalace.tusky.entity.Poll; -import com.keylesspalace.tusky.entity.Status; -import com.keylesspalace.tusky.interfaces.ActionButtonActivity; -import com.keylesspalace.tusky.interfaces.RefreshableFragment; -import com.keylesspalace.tusky.interfaces.ReselectableFragment; -import com.keylesspalace.tusky.interfaces.StatusActionListener; -import com.keylesspalace.tusky.network.MastodonApi; -import com.keylesspalace.tusky.repository.Placeholder; -import com.keylesspalace.tusky.repository.TimelineRepository; -import com.keylesspalace.tusky.repository.TimelineRequestMode; -import com.keylesspalace.tusky.settings.PrefKeys; -import com.keylesspalace.tusky.util.CardViewMode; -import com.keylesspalace.tusky.util.Either; -import com.keylesspalace.tusky.util.HttpHeaderLink; -import com.keylesspalace.tusky.util.LinkHelper; -import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate; -import com.keylesspalace.tusky.util.ListUtils; -import com.keylesspalace.tusky.util.PairedList; -import com.keylesspalace.tusky.util.StatusDisplayOptions; -import com.keylesspalace.tusky.util.StringUtils; -import com.keylesspalace.tusky.util.ViewDataUtils; -import com.keylesspalace.tusky.view.BackgroundMessageView; -import com.keylesspalace.tusky.view.EndlessOnScrollListener; -import com.keylesspalace.tusky.viewdata.StatusViewData; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.ListIterator; -import java.util.Objects; -import java.util.concurrent.TimeUnit; - -import javax.inject.Inject; - -import at.connyduck.sparkbutton.helpers.Utils; -import io.reactivex.Observable; -import io.reactivex.Single; -import io.reactivex.android.schedulers.AndroidSchedulers; -import kotlin.Unit; -import kotlin.collections.CollectionsKt; -import kotlin.jvm.functions.Function1; -import retrofit2.Response; - -import static com.uber.autodispose.AutoDispose.autoDisposable; -import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from; - -public class TimelineFragment extends SFragment implements - SwipeRefreshLayout.OnRefreshListener, - StatusActionListener, - Injectable, ReselectableFragment, RefreshableFragment { - private static final String TAG = "TimelineF"; // logging tag - private static final String KIND_ARG = "kind"; - private static final String ID_ARG = "id"; - private static final String HASHTAGS_ARG = "hastags"; - private static final String ARG_ENABLE_SWIPE_TO_REFRESH = "arg.enable.swipe.to.refresh"; - - private static final int LOAD_AT_ONCE = 30; - private boolean isSwipeToRefreshEnabled = true; - private boolean isNeedRefresh; - - public enum Kind { - HOME, - PUBLIC_LOCAL, - PUBLIC_FEDERATED, - TAG, - USER, - USER_PINNED, - USER_WITH_REPLIES, - FAVOURITES, - LIST, - BOOKMARKS - } - - private enum FetchEnd { - TOP, - BOTTOM, - MIDDLE - } - - @Inject - public EventHub eventHub; - @Inject - TimelineRepository timelineRepo; - - @Inject - public AccountManager accountManager; - - private boolean eventRegistered = false; - - private SwipeRefreshLayout swipeRefreshLayout; - private RecyclerView recyclerView; - private ProgressBar progressBar; - private ContentLoadingProgressBar topProgressBar; - private BackgroundMessageView statusView; - - private TimelineAdapter adapter; - private Kind kind; - private String id; - private List tags; - /** - * For some timeline kinds we must use LINK headers and not just status ids. - */ - private String nextId; - private LinearLayoutManager layoutManager; - private EndlessOnScrollListener scrollListener; - private boolean filterRemoveReplies; - private boolean filterRemoveReblogs; - private boolean hideFab; - private boolean bottomLoading; - - private boolean didLoadEverythingBottom; - private boolean alwaysShowSensitiveMedia; - private boolean alwaysOpenSpoiler; - private boolean initialUpdateFailed = false; - - private PairedList, StatusViewData> statuses = - new PairedList<>(new Function, StatusViewData>() { - @Override - public StatusViewData apply(Either input) { - Status status = input.asRightOrNull(); - if (status != null) { - return ViewDataUtils.statusToViewData( - status, - alwaysShowSensitiveMedia, - alwaysOpenSpoiler - ); - } else { - Placeholder placeholder = input.asLeft(); - return new StatusViewData.Placeholder(placeholder.getId(), false); - } - } - }); - - public static TimelineFragment newInstance(Kind kind) { - return newInstance(kind, null); - } - - public static TimelineFragment newInstance(Kind kind, @Nullable String hashtagOrId) { - return newInstance(kind, hashtagOrId, true); - } - - public static TimelineFragment newInstance(Kind kind, @Nullable String hashtagOrId, boolean enableSwipeToRefresh) { - TimelineFragment fragment = new TimelineFragment(); - Bundle arguments = new Bundle(3); - arguments.putString(KIND_ARG, kind.name()); - arguments.putString(ID_ARG, hashtagOrId); - arguments.putBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, enableSwipeToRefresh); - fragment.setArguments(arguments); - return fragment; - } - - public static TimelineFragment newHashtagInstance(@NonNull List hashtags) { - TimelineFragment fragment = new TimelineFragment(); - Bundle arguments = new Bundle(3); - arguments.putString(KIND_ARG, Kind.TAG.name()); - arguments.putStringArrayList(HASHTAGS_ARG, new ArrayList<>(hashtags)); - arguments.putBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, true); - fragment.setArguments(arguments); - return fragment; - } - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - Bundle arguments = requireArguments(); - kind = Kind.valueOf(arguments.getString(KIND_ARG)); - if (kind == Kind.USER - || kind == Kind.USER_PINNED - || kind == Kind.USER_WITH_REPLIES - || kind == Kind.LIST) { - id = arguments.getString(ID_ARG); - } - if (kind == Kind.TAG) { - tags = arguments.getStringArrayList(HASHTAGS_ARG); - } - - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); - StatusDisplayOptions statusDisplayOptions = new StatusDisplayOptions( - preferences.getBoolean("animateGifAvatars", false), - accountManager.getActiveAccount().getMediaPreviewEnabled(), - preferences.getBoolean("absoluteTimeView", false), - preferences.getBoolean("showBotOverlay", true), - preferences.getBoolean("useBlurhash", true), - preferences.getBoolean("showCardsInTimelines", false) ? - CardViewMode.INDENTED : - CardViewMode.NONE, - preferences.getBoolean("confirmReblogs", true), - preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false), - preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) - ); - adapter = new TimelineAdapter(dataSource, statusDisplayOptions, this); - - isSwipeToRefreshEnabled = arguments.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, true); - - } - - @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - final View rootView = inflater.inflate(R.layout.fragment_timeline, container, false); - - recyclerView = rootView.findViewById(R.id.recyclerView); - swipeRefreshLayout = rootView.findViewById(R.id.swipeRefreshLayout); - progressBar = rootView.findViewById(R.id.progressBar); - statusView = rootView.findViewById(R.id.statusView); - topProgressBar = rootView.findViewById(R.id.topProgressBar); - - setupSwipeRefreshLayout(); - setupRecyclerView(); - updateAdapter(); - setupTimelinePreferences(); - - if (statuses.isEmpty()) { - progressBar.setVisibility(View.VISIBLE); - bottomLoading = true; - this.sendInitialRequest(); - } else { - progressBar.setVisibility(View.GONE); - if (isNeedRefresh) - onRefresh(); - } - - return rootView; - } - - private void sendInitialRequest() { - if (this.kind == Kind.HOME) { - this.tryCache(); - } else { - sendFetchTimelineRequest(null, null, null, FetchEnd.BOTTOM, -1); - } - } - - private void tryCache() { - // Request timeline from disk to make it quick, then replace it with timeline from - // the server to update it - this.timelineRepo.getStatuses(null, null, null, LOAD_AT_ONCE, - TimelineRequestMode.DISK) - .observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) - .subscribe(statuses -> { - filterStatuses(statuses); - - if (statuses.size() > 1) { - this.clearPlaceholdersForResponse(statuses); - this.statuses.clear(); - this.statuses.addAll(statuses); - this.updateAdapter(); - this.progressBar.setVisibility(View.GONE); - // Request statuses including current top to refresh all of them - } - - this.updateCurrent(); - this.loadAbove(); - }); - } - - private void updateCurrent() { - if (this.statuses.isEmpty()) { - return; - } - - String topId = CollectionsKt.first(this.statuses, Either::isRight).asRight().getId(); - - this.timelineRepo.getStatuses(topId, null, null, LOAD_AT_ONCE, - TimelineRequestMode.NETWORK) - .observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) - .subscribe( - (statuses) -> { - this.initialUpdateFailed = false; - // When cached timeline is too old, we would replace it with nothing - if (!statuses.isEmpty()) { - filterStatuses(statuses); - - if (!this.statuses.isEmpty()) { - // clear old cached statuses - Iterator> iterator = this.statuses.iterator(); - while (iterator.hasNext()) { - Either item = iterator.next(); - if (item.isRight()) { - Status status = item.asRight(); - if (status.getId().length() < topId.length() || status.getId().compareTo(topId) < 0) { - - iterator.remove(); - } - } else { - Placeholder placeholder = item.asLeft(); - if (placeholder.getId().length() < topId.length() || placeholder.getId().compareTo(topId) < 0) { - - iterator.remove(); - } - } - - } - } - - this.statuses.addAll(statuses); - this.updateAdapter(); - } - this.bottomLoading = false; - - }, - (e) -> { - this.initialUpdateFailed = true; - // Indicate that we are not loading anymore - this.progressBar.setVisibility(View.GONE); - this.swipeRefreshLayout.setRefreshing(false); - }); - } - - private void setupTimelinePreferences() { - alwaysShowSensitiveMedia = accountManager.getActiveAccount().getAlwaysShowSensitiveMedia(); - alwaysOpenSpoiler = accountManager.getActiveAccount().getAlwaysOpenSpoiler(); - - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getContext()); - boolean filter = preferences.getBoolean("tabFilterHomeReplies", true); - filterRemoveReplies = kind == Kind.HOME && !filter; - - filter = preferences.getBoolean("tabFilterHomeBoosts", true); - filterRemoveReblogs = kind == Kind.HOME && !filter; - reloadFilters(false); - } - - private static boolean filterContextMatchesKind(Kind kind, List filterContext) { - // home, notifications, public, thread - switch (kind) { - case HOME: - case LIST: - return filterContext.contains(Filter.HOME); - case PUBLIC_FEDERATED: - case PUBLIC_LOCAL: - case TAG: - return filterContext.contains(Filter.PUBLIC); - case FAVOURITES: - return (filterContext.contains(Filter.PUBLIC) || filterContext.contains(Filter.NOTIFICATIONS)); - case USER: - case USER_WITH_REPLIES: - case USER_PINNED: - return filterContext.contains(Filter.ACCOUNT); - default: - return false; - } - } - - @Override - protected boolean filterIsRelevant(@NonNull Filter filter) { - return filterContextMatchesKind(kind, filter.getContext()); - } - - @Override - protected void refreshAfterApplyingFilters() { - fullyRefresh(); - } - - private void setupSwipeRefreshLayout() { - swipeRefreshLayout.setEnabled(isSwipeToRefreshEnabled); - if (isSwipeToRefreshEnabled) { - swipeRefreshLayout.setOnRefreshListener(this); - swipeRefreshLayout.setColorSchemeResources(R.color.chinwag_green); - } - } - - private void setupRecyclerView() { - recyclerView.setAccessibilityDelegateCompat( - new ListStatusAccessibilityDelegate(recyclerView, this, statuses::getPairedItemOrNull)); - Context context = recyclerView.getContext(); - recyclerView.setHasFixedSize(true); - layoutManager = new LinearLayoutManager(context); - recyclerView.setLayoutManager(layoutManager); - DividerItemDecoration divider = new DividerItemDecoration( - context, layoutManager.getOrientation()); - recyclerView.addItemDecoration(divider); - - // CWs are expanded without animation, buttons animate itself, we don't need it basically - ((SimpleItemAnimator) recyclerView.getItemAnimator()).setSupportsChangeAnimations(false); - - recyclerView.setAdapter(adapter); - } - - private void deleteStatusById(String id) { - for (int i = 0; i < statuses.size(); i++) { - Either either = statuses.get(i); - if (either.isRight() - && id.equals(either.asRight().getId())) { - statuses.remove(either); - updateAdapter(); - break; - } - } - if (statuses.size() == 0) { - showNothing(); - } - } - - private void showNothing() { - statusView.setVisibility(View.VISIBLE); - statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null); - } - - @Override - public void onActivityCreated(@Nullable Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - /* This is delayed until onActivityCreated solely because MainActivity.composeButton isn't - * guaranteed to be set until then. */ - if (actionButtonPresent()) { - /* Use a modified scroll listener that both loads more statuses as it goes, and hides - * the follow button on down-scroll. */ - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getContext()); - hideFab = preferences.getBoolean("fabHide", false); - scrollListener = new EndlessOnScrollListener(layoutManager) { - @Override - public void onScrolled(RecyclerView view, int dx, int dy) { - super.onScrolled(view, dx, dy); - - ActionButtonActivity activity = (ActionButtonActivity) getActivity(); - FloatingActionButton composeButton = activity.getActionButton(); - - if (composeButton != null) { - if (hideFab) { - if (dy > 0 && composeButton.isShown()) { - composeButton.hide(); // hides the button if we're scrolling down - } else if (dy < 0 && !composeButton.isShown()) { - composeButton.show(); // shows it if we are scrolling up - } - } else if (!composeButton.isShown()) { - composeButton.show(); - } - } - } - - @Override - public void onLoadMore(int totalItemsCount, RecyclerView view) { - TimelineFragment.this.onLoadMore(); - } - }; - } else { - // Just use the basic scroll listener to load more statuses. - scrollListener = new EndlessOnScrollListener(layoutManager) { - @Override - public void onLoadMore(int totalItemsCount, RecyclerView view) { - TimelineFragment.this.onLoadMore(); - } - }; - } - recyclerView.addOnScrollListener(scrollListener); - - if (!eventRegistered) { - eventHub.getEvents() - .observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) - .subscribe(event -> { - if (event instanceof FavoriteEvent) { - FavoriteEvent favEvent = ((FavoriteEvent) event); - handleFavEvent(favEvent); - } else if (event instanceof ReblogEvent) { - ReblogEvent reblogEvent = (ReblogEvent) event; - handleReblogEvent(reblogEvent); - } else if (event instanceof BookmarkEvent) { - BookmarkEvent bookmarkEvent = (BookmarkEvent) event; - handleBookmarkEvent(bookmarkEvent); - } else if (event instanceof MuteConversationEvent) { - MuteConversationEvent muteEvent = (MuteConversationEvent) event; - handleMuteConversationEvent(muteEvent); - } else if (event instanceof UnfollowEvent) { - if (kind == Kind.HOME) { - String id = ((UnfollowEvent) event).getAccountId(); - removeAllByAccountId(id); - } - } else if (event instanceof BlockEvent) { - if (kind != Kind.USER && kind != Kind.USER_WITH_REPLIES && kind != Kind.USER_PINNED) { - String id = ((BlockEvent) event).getAccountId(); - removeAllByAccountId(id); - } - } else if (event instanceof MuteEvent) { - if (kind != Kind.USER && kind != Kind.USER_WITH_REPLIES && kind != Kind.USER_PINNED) { - String id = ((MuteEvent) event).getAccountId(); - removeAllByAccountId(id); - } - } else if (event instanceof DomainMuteEvent) { - if (kind != Kind.USER && kind != Kind.USER_WITH_REPLIES && kind != Kind.USER_PINNED) { - String instance = ((DomainMuteEvent) event).getInstance(); - removeAllByInstance(instance); - } - } else if (event instanceof StatusDeletedEvent) { - if (kind != Kind.USER && kind != Kind.USER_WITH_REPLIES && kind != Kind.USER_PINNED) { - String id = ((StatusDeletedEvent) event).getStatusId(); - deleteStatusById(id); - } - } else if (event instanceof StatusComposedEvent) { - Status status = ((StatusComposedEvent) event).getStatus(); - handleStatusComposeEvent(status); - } else if (event instanceof PreferenceChangedEvent) { - onPreferenceChanged(((PreferenceChangedEvent) event).getPreferenceKey()); - } - }); - eventRegistered = true; - } - } - - @Override - public void onRefresh() { - if (isSwipeToRefreshEnabled) - swipeRefreshLayout.setEnabled(true); - this.statusView.setVisibility(View.GONE); - isNeedRefresh = false; - if (this.initialUpdateFailed) { - updateCurrent(); - } - - this.loadAbove(); - - } - - private void loadAbove() { - String firstOrNull = null; - String secondOrNull = null; - for (int i = 0; i < this.statuses.size(); i++) { - Either status = this.statuses.get(i); - if (status.isRight()) { - firstOrNull = status.asRight().getId(); - if (i + 1 < statuses.size() && statuses.get(i + 1).isRight()) { - secondOrNull = statuses.get(i + 1).asRight().getId(); - } - break; - } - } - if (firstOrNull != null) { - this.sendFetchTimelineRequest(null, firstOrNull, secondOrNull, FetchEnd.TOP, -1); - } else { - this.sendFetchTimelineRequest(null, null, null, FetchEnd.BOTTOM, -1); - } - } - - @Override - public void onReply(int position) { - super.reply(statuses.get(position).asRight()); - } - - @Override - public void onReblog(final boolean reblog, final int position) { - final Status status = statuses.get(position).asRight(); - timelineCases.reblog(status, reblog) - .observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) - .subscribe( - (newStatus) -> setRebloggedForStatus(position, status, reblog), - (err) -> Log.d(TAG, "Failed to reblog status " + status.getId(), err) - ); - } - - private void setRebloggedForStatus(int position, Status status, boolean reblog) { - status.setReblogged(reblog); - - if (status.getReblog() != null) { - status.getReblog().setReblogged(reblog); - } - - Pair actual = - findStatusAndPosition(position, status); - if (actual == null) return; - - StatusViewData newViewData = - new StatusViewData.Builder(actual.first) - .setReblogged(reblog) - .createStatusViewData(); - statuses.setPairedItem(actual.second, newViewData); - updateAdapter(); - } - - @Override - public void onFavourite(final boolean favourite, final int position) { - final Status status = statuses.get(position).asRight(); - - timelineCases.favourite(status, favourite) - .observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) - .subscribe( - (newStatus) -> setFavouriteForStatus(position, newStatus, favourite), - (err) -> Log.d(TAG, "Failed to favourite status " + status.getId(), err) - ); - } - - private void setFavouriteForStatus(int position, Status status, boolean favourite) { - status.setFavourited(favourite); - - if (status.getReblog() != null) { - status.getReblog().setFavourited(favourite); - } - - Pair actual = - findStatusAndPosition(position, status); - if (actual == null) return; - - StatusViewData newViewData = new StatusViewData - .Builder(actual.first) - .setFavourited(favourite) - .createStatusViewData(); - statuses.setPairedItem(actual.second, newViewData); - updateAdapter(); - } - - @Override - public void onBookmark(final boolean bookmark, final int position) { - final Status status = statuses.get(position).asRight(); - - timelineCases.bookmark(status, bookmark) - .observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) - .subscribe( - (newStatus) -> setBookmarkForStatus(position, newStatus, bookmark), - (err) -> Log.d(TAG, "Failed to favourite status " + status.getId(), err) - ); - } - - private void setBookmarkForStatus(int position, Status status, boolean bookmark) { - status.setBookmarked(bookmark); - - if (status.getReblog() != null) { - status.getReblog().setBookmarked(bookmark); - } - - Pair actual = - findStatusAndPosition(position, status); - if (actual == null) return; - - StatusViewData newViewData = new StatusViewData - .Builder(actual.first) - .setBookmarked(bookmark) - .createStatusViewData(); - statuses.setPairedItem(actual.second, newViewData); - updateAdapter(); - } - - public void onVoteInPoll(int position, @NonNull List choices) { - - final Status status = statuses.get(position).asRight(); - - Poll votedPoll = status.getActionableStatus().getPoll().votedCopy(choices); - - setVoteForPoll(position, status, votedPoll); - - timelineCases.voteInPoll(status, choices) - .observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this))) - .subscribe( - (newPoll) -> setVoteForPoll(position, status, newPoll), - (t) -> Log.d(TAG, - "Failed to vote in poll: " + status.getId(), t) - ); - } - - private void setVoteForPoll(int position, Status status, Poll newPoll) { - Pair actual = - findStatusAndPosition(position, status); - if (actual == null) return; - - StatusViewData newViewData = new StatusViewData - .Builder(actual.first) - .setPoll(newPoll) - .createStatusViewData(); - statuses.setPairedItem(actual.second, newViewData); - updateAdapter(); - } - - @Override - public void onMore(@NonNull View view, final int position) { - super.more(statuses.get(position).asRight(), view, position); - } - - @Override - public void onOpenReblog(int position) { - super.openReblog(statuses.get(position).asRight()); - } - - @Override - public void onExpandedChange(boolean expanded, int position) { - StatusViewData newViewData = new StatusViewData.Builder( - ((StatusViewData.Concrete) statuses.getPairedItem(position))) - .setIsExpanded(expanded).createStatusViewData(); - statuses.setPairedItem(position, newViewData); - updateAdapter(); - } - - @Override - public void onContentHiddenChange(boolean isShowing, int position) { - StatusViewData newViewData = new StatusViewData.Builder( - ((StatusViewData.Concrete) statuses.getPairedItem(position))) - .setIsShowingSensitiveContent(isShowing).createStatusViewData(); - statuses.setPairedItem(position, newViewData); - updateAdapter(); - } - - - @Override - public void onShowReblogs(int position) { - String statusId = statuses.get(position).asRight().getId(); - Intent intent = AccountListActivity.newIntent(getContext(), AccountListActivity.Type.REBLOGGED, statusId); - ((BaseActivity) getActivity()).startActivityWithSlideInAnimation(intent); - } - - @Override - public void onShowFavs(int position) { - String statusId = statuses.get(position).asRight().getId(); - Intent intent = AccountListActivity.newIntent(getContext(), AccountListActivity.Type.FAVOURITED, statusId); - ((BaseActivity) getActivity()).startActivityWithSlideInAnimation(intent); - } - - @Override - public void onLoadMore(int position) { - //check bounds before accessing list, - if (statuses.size() >= position && position > 0) { - Status fromStatus = statuses.get(position - 1).asRightOrNull(); - Status toStatus = statuses.get(position + 1).asRightOrNull(); - String maxMinusOne = - statuses.size() > position + 1 && statuses.get(position + 2).isRight() - ? statuses.get(position + 1).asRight().getId() - : null; - if (fromStatus == null || toStatus == null) { - Log.e(TAG, "Failed to load more at " + position + ", wrong placeholder position"); - return; - } - sendFetchTimelineRequest(fromStatus.getId(), toStatus.getId(), maxMinusOne, - FetchEnd.MIDDLE, position); - - Placeholder placeholder = statuses.get(position).asLeft(); - StatusViewData newViewData = new StatusViewData.Placeholder(placeholder.getId(), true); - statuses.setPairedItem(position, newViewData); - updateAdapter(); - } else { - Log.e(TAG, "error loading more"); - } - } - - @Override - public void onContentCollapsedChange(boolean isCollapsed, int position) { - if (position < 0 || position >= statuses.size()) { - Log.e(TAG, String.format("Tried to access out of bounds status position: %d of %d", position, statuses.size() - 1)); - return; - } - - StatusViewData status = statuses.getPairedItem(position); - if (!(status instanceof StatusViewData.Concrete)) { - // Statuses PairedList contains a base type of StatusViewData.Concrete and also doesn't - // check for null values when adding values to it although this doesn't seem to be an issue. - Log.e(TAG, String.format( - "Expected StatusViewData.Concrete, got %s instead at position: %d of %d", - status == null ? "" : status.getClass().getSimpleName(), - position, - statuses.size() - 1 - )); - return; - } - - StatusViewData updatedStatus = new StatusViewData.Builder((StatusViewData.Concrete) status) - .setCollapsed(isCollapsed) - .createStatusViewData(); - statuses.setPairedItem(position, updatedStatus); - updateAdapter(); - } - - @Override - public void onViewMedia(int position, int attachmentIndex, @Nullable View view) { - Status status = statuses.get(position).asRightOrNull(); - if (status == null) return; - super.viewMedia(attachmentIndex, status, view); - } - - @Override - public void onViewThread(int position) { - super.viewThread(statuses.get(position).asRight()); - } - - @Override - public void onViewTag(String tag) { - if (kind == Kind.TAG && tags.size() == 1 && tags.contains(tag)) { - // If already viewing a tag page, then ignore any request to view that tag again. - return; - } - super.viewTag(tag); - } - - @Override - public void onViewAccount(String id) { - if ((kind == Kind.USER || kind == Kind.USER_WITH_REPLIES) && this.id.equals(id)) { - /* If already viewing an account page, then any requests to view that account page - * should be ignored. */ - return; - } - super.viewAccount(id); - } - - private void onPreferenceChanged(String key) { - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); - switch (key) { - case "fabHide": { - hideFab = sharedPreferences.getBoolean("fabHide", false); - break; - } - case "mediaPreviewEnabled": { - boolean enabled = accountManager.getActiveAccount().getMediaPreviewEnabled(); - boolean oldMediaPreviewEnabled = adapter.getMediaPreviewEnabled(); - if (enabled != oldMediaPreviewEnabled) { - adapter.setMediaPreviewEnabled(enabled); - fullyRefresh(); - } - break; - } - case "tabFilterHomeReplies": { - boolean filter = sharedPreferences.getBoolean("tabFilterHomeReplies", true); - boolean oldRemoveReplies = filterRemoveReplies; - filterRemoveReplies = kind == Kind.HOME && !filter; - if (adapter.getItemCount() > 1 && oldRemoveReplies != filterRemoveReplies) { - fullyRefresh(); - } - break; - } - case "tabFilterHomeBoosts": { - boolean filter = sharedPreferences.getBoolean("tabFilterHomeBoosts", true); - boolean oldRemoveReblogs = filterRemoveReblogs; - filterRemoveReblogs = kind == Kind.HOME && !filter; - if (adapter.getItemCount() > 1 && oldRemoveReblogs != filterRemoveReblogs) { - fullyRefresh(); - } - break; - } - case Filter.HOME: - case Filter.NOTIFICATIONS: - case Filter.THREAD: - case Filter.PUBLIC: - case Filter.ACCOUNT: { - if (filterContextMatchesKind(kind, Collections.singletonList(key))) { - reloadFilters(true); - } - break; - } - case "alwaysShowSensitiveMedia": { - //it is ok if only newly loaded statuses are affected, no need to fully refresh - alwaysShowSensitiveMedia = accountManager.getActiveAccount().getAlwaysShowSensitiveMedia(); - break; - } - } - } - - @Override - public void removeItem(int position) { - statuses.remove(position); - updateAdapter(); - } - - private void removeAllByAccountId(String accountId) { - // using iterator to safely remove items while iterating - Iterator> iterator = statuses.iterator(); - while (iterator.hasNext()) { - Status status = iterator.next().asRightOrNull(); - if (status != null && - (status.getAccount().getId().equals(accountId) || status.getActionableStatus().getAccount().getId().equals(accountId))) { - iterator.remove(); - } - } - updateAdapter(); - } - - private void removeAllByInstance(String instance) { - // using iterator to safely remove items while iterating - Iterator> iterator = statuses.iterator(); - while (iterator.hasNext()) { - Status status = iterator.next().asRightOrNull(); - if (status != null && LinkHelper.getDomain(status.getAccount().getUrl()).equals(instance)) { - iterator.remove(); - } - } - updateAdapter(); - } - - private void onLoadMore() { - if (didLoadEverythingBottom || bottomLoading) { - return; - } - - if (statuses.size() == 0) { - sendInitialRequest(); - return; - } - - bottomLoading = true; - - Either last = statuses.get(statuses.size() - 1); - Placeholder placeholder; - if (last.isRight()) { - final String placeholderId = StringUtils.dec(last.asRight().getId()); - placeholder = new Placeholder(placeholderId); - statuses.add(new Either.Left<>(placeholder)); - } else { - placeholder = last.asLeft(); - } - statuses.setPairedItem(statuses.size() - 1, - new StatusViewData.Placeholder(placeholder.getId(), true)); - - updateAdapter(); - - String bottomId = null; - if (kind == Kind.FAVOURITES || kind == Kind.BOOKMARKS) { - bottomId = this.nextId; - } else { - final ListIterator> iterator = - this.statuses.listIterator(this.statuses.size()); - while (iterator.hasPrevious()) { - Either previous = iterator.previous(); - if (previous.isRight()) { - bottomId = previous.asRight().getId(); - break; - } - } - } - sendFetchTimelineRequest(bottomId, null, null, FetchEnd.BOTTOM, -1); - } - - private void fullyRefresh() { - statuses.clear(); - updateAdapter(); - bottomLoading = true; - sendFetchTimelineRequest(null, null, null, FetchEnd.BOTTOM, -1); - } - - private boolean actionButtonPresent() { - return kind != Kind.TAG && kind != Kind.FAVOURITES && kind != Kind.BOOKMARKS && - getActivity() instanceof ActionButtonActivity; - } - - private void jumpToTop() { - if (isAdded()) { - layoutManager.scrollToPosition(0); - recyclerView.stopScroll(); - scrollListener.reset(); - } - } - - private Single>> getFetchCallByTimelineType(String fromId, String uptoId) { - MastodonApi api = mastodonApi; - switch (kind) { - default: - case HOME: - return api.homeTimeline(fromId, uptoId, LOAD_AT_ONCE); - case PUBLIC_FEDERATED: - return api.publicTimeline(null, fromId, uptoId, LOAD_AT_ONCE); - case PUBLIC_LOCAL: - return api.publicTimeline(true, fromId, uptoId, LOAD_AT_ONCE); - case TAG: - String firstHashtag = tags.get(0); - List additionalHashtags = tags.subList(1, tags.size()); - return api.hashtagTimeline(firstHashtag, additionalHashtags, null, fromId, uptoId, LOAD_AT_ONCE); - case USER: - return api.accountStatuses(id, fromId, uptoId, LOAD_AT_ONCE, true, null, null); - case USER_PINNED: - return api.accountStatuses(id, fromId, uptoId, LOAD_AT_ONCE, null, null, true); - case USER_WITH_REPLIES: - return api.accountStatuses(id, fromId, uptoId, LOAD_AT_ONCE, null, null, null); - case FAVOURITES: - return api.favourites(fromId, uptoId, LOAD_AT_ONCE); - case BOOKMARKS: - return api.bookmarks(fromId, uptoId, LOAD_AT_ONCE); - case LIST: - return api.listTimeline(id, fromId, uptoId, LOAD_AT_ONCE); - } - } - - private void sendFetchTimelineRequest(@Nullable String maxId, @Nullable String sinceId, - @Nullable String sinceIdMinusOne, - final FetchEnd fetchEnd, final int pos) { - if (isAdded() && (fetchEnd == FetchEnd.TOP || fetchEnd == FetchEnd.BOTTOM && maxId == null && progressBar.getVisibility() != View.VISIBLE) && !isSwipeToRefreshEnabled) - topProgressBar.show(); - - if (kind == Kind.HOME) { - TimelineRequestMode mode; - // allow getting old statuses/fallbacks for network only for for bottom loading - if (fetchEnd == FetchEnd.BOTTOM) { - mode = TimelineRequestMode.ANY; - } else { - mode = TimelineRequestMode.NETWORK; - } - timelineRepo.getStatuses(maxId, sinceId, sinceIdMinusOne, LOAD_AT_ONCE, mode) - .observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) - .subscribe( - result -> onFetchTimelineSuccess(result, fetchEnd, pos), - err -> onFetchTimelineFailure(err, fetchEnd, pos) - ); - } else { - getFetchCallByTimelineType(maxId, sinceId) - .observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) - .subscribe( - response -> { - if (response.isSuccessful()) { - @Nullable - String newNextId = extractNextId(response); - if (newNextId != null) { - // when we reach the bottom of the list, we won't have a new link. If - // we blindly write `null` here we will start loading from the top - // again. - nextId = newNextId; - } - onFetchTimelineSuccess(liftStatusList(response.body()), fetchEnd, pos); - } else { - onFetchTimelineFailure(new Exception(response.message()), fetchEnd, pos); - } - }, - err -> onFetchTimelineFailure(err, fetchEnd, pos) - ); - } - } - - @Nullable - private String extractNextId(Response response) { - String linkHeader = response.headers().get("Link"); - if (linkHeader == null) { - return null; - } - List links = HttpHeaderLink.parse(linkHeader); - HttpHeaderLink nextHeader = HttpHeaderLink.findByRelationType(links, "next"); - if (nextHeader == null) { - return null; - } - Uri nextLink = nextHeader.uri; - if (nextLink == null) { - return null; - } - return nextLink.getQueryParameter("max_id"); - } - - private void onFetchTimelineSuccess(List> statuses, - FetchEnd fetchEnd, int pos) { - - // We filled the hole (or reached the end) if the server returned less statuses than we - // we asked for. - boolean fullFetch = statuses.size() >= LOAD_AT_ONCE; - filterStatuses(statuses); - switch (fetchEnd) { - case TOP: { - updateStatuses(statuses, fullFetch); - break; - } - case MIDDLE: { - replacePlaceholderWithStatuses(statuses, fullFetch, pos); - break; - } - case BOTTOM: { - if (!this.statuses.isEmpty() - && !this.statuses.get(this.statuses.size() - 1).isRight()) { - this.statuses.remove(this.statuses.size() - 1); - updateAdapter(); - } - - if (!statuses.isEmpty() && !statuses.get(statuses.size() - 1).isRight()) { - // Removing placeholder if it's the last one from the cache - statuses.remove(statuses.size() - 1); - } - int oldSize = this.statuses.size(); - if (this.statuses.size() > 1) { - addItems(statuses); - } else { - updateStatuses(statuses, fullFetch); - } - if (this.statuses.size() == oldSize) { - // This may be a brittle check but seems like it works - // Can we check it using headers somehow? Do all server support them? - didLoadEverythingBottom = true; - } - break; - } - } - if (isAdded()) { - topProgressBar.hide(); - updateBottomLoadingState(fetchEnd); - progressBar.setVisibility(View.GONE); - swipeRefreshLayout.setRefreshing(false); - swipeRefreshLayout.setEnabled(true); - if (this.statuses.size() == 0) { - this.showNothing(); - } else { - this.statusView.setVisibility(View.GONE); - } - } - } - - private void onFetchTimelineFailure(Throwable throwable, FetchEnd fetchEnd, int position) { - if (isAdded()) { - swipeRefreshLayout.setRefreshing(false); - topProgressBar.hide(); - - if (fetchEnd == FetchEnd.MIDDLE && !statuses.get(position).isRight()) { - Placeholder placeholder = statuses.get(position).asLeftOrNull(); - StatusViewData newViewData; - if (placeholder == null) { - Status above = statuses.get(position - 1).asRight(); - String newId = StringUtils.dec(above.getId()); - placeholder = new Placeholder(newId); - } - newViewData = new StatusViewData.Placeholder(placeholder.getId(), false); - statuses.setPairedItem(position, newViewData); - updateAdapter(); - } else if (this.statuses.isEmpty()) { - swipeRefreshLayout.setEnabled(false); - this.statusView.setVisibility(View.VISIBLE); - if (throwable instanceof IOException) { - this.statusView.setup(R.drawable.elephant_offline, R.string.error_network, __ -> { - this.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); - this.onRefresh(); - return Unit.INSTANCE; - }); - } - } - - Log.e(TAG, "Fetch Failure: " + throwable.getMessage()); - updateBottomLoadingState(fetchEnd); - progressBar.setVisibility(View.GONE); - } - } - - private void updateBottomLoadingState(FetchEnd fetchEnd) { - if (fetchEnd == FetchEnd.BOTTOM) { - bottomLoading = false; - } - } - - private void filterStatuses(List> statuses) { - Iterator> it = statuses.iterator(); - while (it.hasNext()) { - Status status = it.next().asRightOrNull(); - if (status != null - && ((status.getInReplyToId() != null && filterRemoveReplies) - || (status.getReblog() != null && filterRemoveReblogs) - || shouldFilterStatus(status.getActionableStatus()))) { - it.remove(); - } - } - } - - private void updateStatuses(List> newStatuses, boolean fullFetch) { - if (ListUtils.isEmpty(newStatuses)) { - updateAdapter(); - return; - } - - if (statuses.isEmpty()) { - statuses.addAll(newStatuses); - } else { - Either lastOfNew = newStatuses.get(newStatuses.size() - 1); - int index = statuses.indexOf(lastOfNew); - - if (index >= 0) { - statuses.subList(0, index).clear(); - } - - int newIndex = newStatuses.indexOf(statuses.get(0)); - if (newIndex == -1) { - if (index == -1 && fullFetch) { - String placeholderId = StringUtils.inc( - CollectionsKt.last(newStatuses, Either::isRight).asRight().getId()); - newStatuses.add(new Either.Left<>(new Placeholder(placeholderId))); - } - statuses.addAll(0, newStatuses); - } else { - statuses.addAll(0, newStatuses.subList(0, newIndex)); - } - } - // Remove all consecutive placeholders - removeConsecutivePlaceholders(); - updateAdapter(); - } - - private void removeConsecutivePlaceholders() { - for (int i = 0; i < statuses.size() - 1; i++) { - if (statuses.get(i).isLeft() && statuses.get(i + 1).isLeft()) { - statuses.remove(i); - } - } - } - - private void addItems(List> newStatuses) { - if (ListUtils.isEmpty(newStatuses)) { - return; - } - Either last = null; - for (int i = statuses.size() - 1; i >= 0; i--) { - if (statuses.get(i).isRight()) { - last = statuses.get(i); - break; - } - } - // I was about to replace findStatus with indexOf but it is incorrect to compare value - // types by ID anyway and we should change equals() for Status, I think, so this makes sense - if (last != null && !newStatuses.contains(last)) { - statuses.addAll(newStatuses); - removeConsecutivePlaceholders(); - updateAdapter(); - } - } - - /** - * For certain requests we don't want to see placeholders, they will be removed some other way - */ - private void clearPlaceholdersForResponse(List> statuses) { - CollectionsKt.removeAll(statuses, Either::isLeft); - } - - private void replacePlaceholderWithStatuses(List> newStatuses, - boolean fullFetch, int pos) { - Either placeholder = statuses.get(pos); - if (placeholder.isLeft()) { - statuses.remove(pos); - } - - if (ListUtils.isEmpty(newStatuses)) { - updateAdapter(); - return; - } - - if (fullFetch) { - newStatuses.add(placeholder); - } - - statuses.addAll(pos, newStatuses); - removeConsecutivePlaceholders(); - - updateAdapter(); - - } - - private int findStatusOrReblogPositionById(@NonNull String statusId) { - for (int i = 0; i < statuses.size(); i++) { - Status status = statuses.get(i).asRightOrNull(); - if (status != null - && (statusId.equals(status.getId()) - || (status.getReblog() != null - && statusId.equals(status.getReblog().getId())))) { - return i; - } - } - return -1; - } - - private final Function1> statusLifter = - Either.Right::new; - - @Nullable - private Pair - findStatusAndPosition(int position, Status status) { - StatusViewData.Concrete statusToUpdate; - int positionToUpdate; - StatusViewData someOldViewData = statuses.getPairedItem(position); - - // Unlikely, but data could change between the request and response - if ((someOldViewData instanceof StatusViewData.Placeholder) || - !((StatusViewData.Concrete) someOldViewData).getId().equals(status.getId())) { - // try to find the status we need to update - int foundPos = statuses.indexOf(new Either.Right<>(status)); - if (foundPos < 0) return null; // okay, it's hopeless, give up - statusToUpdate = ((StatusViewData.Concrete) - statuses.getPairedItem(foundPos)); - positionToUpdate = position; - } else { - statusToUpdate = (StatusViewData.Concrete) someOldViewData; - positionToUpdate = position; - } - return new Pair<>(statusToUpdate, positionToUpdate); - } - - private void handleReblogEvent(@NonNull ReblogEvent reblogEvent) { - int pos = findStatusOrReblogPositionById(reblogEvent.getStatusId()); - if (pos < 0) return; - Status status = statuses.get(pos).asRight(); - setRebloggedForStatus(pos, status, reblogEvent.getReblog()); - } - - private void handleFavEvent(@NonNull FavoriteEvent favEvent) { - int pos = findStatusOrReblogPositionById(favEvent.getStatusId()); - if (pos < 0) return; - Status status = statuses.get(pos).asRight(); - setFavouriteForStatus(pos, status, favEvent.getFavourite()); - } - - private void handleBookmarkEvent(@NonNull BookmarkEvent bookmarkEvent) { - int pos = findStatusOrReblogPositionById(bookmarkEvent.getStatusId()); - if (pos < 0) return; - Status status = statuses.get(pos).asRight(); - setBookmarkForStatus(pos, status, bookmarkEvent.getBookmark()); - } - - private void handleMuteConversationEvent(@NonNull MuteConversationEvent event) { - fullyRefresh(); - } - - private void handleStatusComposeEvent(@NonNull Status status) { - switch (kind) { - case HOME: - case PUBLIC_FEDERATED: - case PUBLIC_LOCAL: - break; - case USER: - case USER_WITH_REPLIES: - if (status.getAccount().getId().equals(id)) { - break; - } else { - return; - } - case TAG: - case FAVOURITES: - case LIST: - return; - } - onRefresh(); - } - - private List> liftStatusList(List list) { - return CollectionsKt.map(list, statusLifter); - } - - private void updateAdapter() { - differ.submitList(statuses.getPairedCopy()); - } - - private final ListUpdateCallback listUpdateCallback = new ListUpdateCallback() { - @Override - public void onInserted(int position, int count) { - if (isAdded()) { - adapter.notifyItemRangeInserted(position, count); - Context context = getContext(); - // scroll up when new items at the top are loaded while being in the first position - // https://github.com/tuskyapp/Tusky/pull/1905#issuecomment-677819724 - if (position == 0 && context != null && adapter.getItemCount() != count) { - if (isSwipeToRefreshEnabled) - recyclerView.scrollBy(0, Utils.dpToPx(context, -30)); - else - recyclerView.scrollToPosition(0); - } - } - } - - @Override - public void onRemoved(int position, int count) { - adapter.notifyItemRangeRemoved(position, count); - } - - @Override - public void onMoved(int fromPosition, int toPosition) { - adapter.notifyItemMoved(fromPosition, toPosition); - } - - @Override - public void onChanged(int position, int count, Object payload) { - adapter.notifyItemRangeChanged(position, count, payload); - } - }; - - - private final AsyncListDiffer - differ = new AsyncListDiffer<>(listUpdateCallback, - new AsyncDifferConfig.Builder<>(diffCallback).build()); - - private final TimelineAdapter.AdapterDataSource dataSource = - new TimelineAdapter.AdapterDataSource() { - @Override - public int getItemCount() { - return differ.getCurrentList().size(); - } - - @Override - public StatusViewData getItemAt(int pos) { - return differ.getCurrentList().get(pos); - } - }; - - private static final DiffUtil.ItemCallback diffCallback - = new DiffUtil.ItemCallback() { - - @Override - public boolean areItemsTheSame(StatusViewData oldItem, StatusViewData newItem) { - return oldItem.getViewDataId() == newItem.getViewDataId(); - } - - @Override - public boolean areContentsTheSame(StatusViewData oldItem, @NonNull StatusViewData newItem) { - return false; //Items are different always. It allows to refresh timestamp on every view holder update - } - - @Nullable - @Override - public Object getChangePayload(@NonNull StatusViewData oldItem, @NonNull StatusViewData newItem) { - if (oldItem.deepEquals(newItem)) { - //If items are equal - update timestamp only - return Collections.singletonList(StatusBaseViewHolder.Key.KEY_CREATED); - } else - // If items are different - update a whole view holder - return null; - } - }; - - AccessibilityManager a11yManager; - boolean talkBackWasEnabled; - - @Override - public void onResume() { - super.onResume(); - a11yManager = Objects.requireNonNull( - ContextCompat.getSystemService(requireContext(), AccessibilityManager.class) - ); - boolean wasEnabled = this.talkBackWasEnabled; - talkBackWasEnabled = a11yManager.isEnabled(); - Log.d(TAG, "talkback was enabled: " + wasEnabled + ", now " + talkBackWasEnabled); - if (talkBackWasEnabled && !wasEnabled) { - this.adapter.notifyDataSetChanged(); - } - startUpdateTimestamp(); - } - - /** - * Start to update adapter every minute to refresh timestamp - * If setting absoluteTimeView is false - * Auto dispose observable on pause - */ - private void startUpdateTimestamp() { - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); - boolean useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false); - if (!useAbsoluteTime) { - Observable.interval(1, TimeUnit.MINUTES) - .observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this, Lifecycle.Event.ON_PAUSE))) - .subscribe( - interval -> updateAdapter() - ); - } - - } - - @Override - public void onReselect() { - jumpToTop(); - } - - @Override - public void refreshContent() { - if (isAdded()) - onRefresh(); - else - isNeedRefresh = true; - } -} diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.kt new file mode 100644 index 00000000..7e91d230 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.kt @@ -0,0 +1,1265 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.fragment + +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.accessibility.AccessibilityManager +import androidx.core.content.ContextCompat +import androidx.core.util.Pair +import androidx.lifecycle.Lifecycle +import androidx.preference.PreferenceManager +import androidx.recyclerview.widget.AsyncDifferConfig +import androidx.recyclerview.widget.AsyncListDiffer +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.ListUpdateCallback +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.SimpleItemAnimator +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener +import at.connyduck.sparkbutton.helpers.Utils +import com.keylesspalace.tusky.AccountListActivity +import com.keylesspalace.tusky.AccountListActivity.Companion.newIntent +import com.keylesspalace.tusky.BaseActivity +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.adapter.StatusBaseViewHolder +import com.keylesspalace.tusky.adapter.TimelineAdapter +import com.keylesspalace.tusky.appstore.BlockEvent +import com.keylesspalace.tusky.appstore.BookmarkEvent +import com.keylesspalace.tusky.appstore.DomainMuteEvent +import com.keylesspalace.tusky.appstore.Event +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.appstore.FavoriteEvent +import com.keylesspalace.tusky.appstore.MuteConversationEvent +import com.keylesspalace.tusky.appstore.MuteEvent +import com.keylesspalace.tusky.appstore.PreferenceChangedEvent +import com.keylesspalace.tusky.appstore.ReblogEvent +import com.keylesspalace.tusky.appstore.StatusComposedEvent +import com.keylesspalace.tusky.appstore.StatusDeletedEvent +import com.keylesspalace.tusky.appstore.UnfollowEvent +import com.keylesspalace.tusky.databinding.FragmentTimelineBinding +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.di.Injectable +import com.keylesspalace.tusky.entity.Filter +import com.keylesspalace.tusky.entity.Poll +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.interfaces.ActionButtonActivity +import com.keylesspalace.tusky.interfaces.RefreshableFragment +import com.keylesspalace.tusky.interfaces.ReselectableFragment +import com.keylesspalace.tusky.interfaces.StatusActionListener +import com.keylesspalace.tusky.repository.Placeholder +import com.keylesspalace.tusky.repository.TimelineRepository +import com.keylesspalace.tusky.repository.TimelineRequestMode +import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.util.CardViewMode +import com.keylesspalace.tusky.util.Either +import com.keylesspalace.tusky.util.Either.Left +import com.keylesspalace.tusky.util.Either.Right +import com.keylesspalace.tusky.util.HttpHeaderLink +import com.keylesspalace.tusky.util.LinkHelper +import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate +import com.keylesspalace.tusky.util.PairedList +import com.keylesspalace.tusky.util.StatusDisplayOptions +import com.keylesspalace.tusky.util.ViewDataUtils +import com.keylesspalace.tusky.util.dec +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.inc +import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.viewBinding +import com.keylesspalace.tusky.view.EndlessOnScrollListener +import com.keylesspalace.tusky.viewdata.StatusViewData +import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from +import com.uber.autodispose.autoDispose +import io.reactivex.Observable +import io.reactivex.Single +import io.reactivex.android.schedulers.AndroidSchedulers +import retrofit2.Response +import java.io.IOException +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +class TimelineFragment : SFragment(), OnRefreshListener, StatusActionListener, Injectable, ReselectableFragment, RefreshableFragment { + + @Inject + lateinit var eventHub: EventHub + + @Inject + lateinit var timelineRepo: TimelineRepository + + @Inject + lateinit var accountManager: AccountManager + + private val binding by viewBinding(FragmentTimelineBinding::bind) + + private var kind: Kind? = null + private var id: String? = null + private var tags: List = emptyList() + + private lateinit var adapter: TimelineAdapter + + private var isSwipeToRefreshEnabled = true + private var isNeedRefresh = false + + private var eventRegistered = false + + /** + * For some timeline kinds we must use LINK headers and not just status ids. + */ + private var nextId: String? = null + private var layoutManager: LinearLayoutManager? = null + private var scrollListener: EndlessOnScrollListener? = null + private var filterRemoveReplies = false + private var filterRemoveReblogs = false + private var hideFab = false + private var bottomLoading = false + private var didLoadEverythingBottom = false + private var alwaysShowSensitiveMedia = false + private var alwaysOpenSpoiler = false + private var initialUpdateFailed = false + + private val statuses = PairedList, StatusViewData> { input -> + val status = input.asRightOrNull() + if (status != null) { + ViewDataUtils.statusToViewData( + status, + alwaysShowSensitiveMedia, + alwaysOpenSpoiler + ) + } else { + val (id1) = input.asLeft() + StatusViewData.Placeholder(id1, false) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val arguments = requireArguments() + kind = Kind.valueOf(arguments.getString(KIND_ARG)!!) + if (kind == Kind.USER || kind == Kind.USER_PINNED || kind == Kind.USER_WITH_REPLIES || kind == Kind.LIST) { + id = arguments.getString(ID_ARG)!! + } + if (kind == Kind.TAG) { + tags = arguments.getStringArrayList(HASHTAGS_ARG)!! + } + + isSwipeToRefreshEnabled = arguments.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, true) + + val preferences = PreferenceManager.getDefaultSharedPreferences(activity) + val statusDisplayOptions = StatusDisplayOptions( + animateAvatars = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false), + mediaPreviewEnabled = accountManager.activeAccount!!.mediaPreviewEnabled, + useAbsoluteTime = preferences.getBoolean(PrefKeys.ABSOLUTE_TIME_VIEW, false), + showBotOverlay = preferences.getBoolean(PrefKeys.SHOW_BOT_OVERLAY, true), + useBlurhash = preferences.getBoolean(PrefKeys.USE_BLURHASH, true), + cardViewMode = if (preferences.getBoolean(PrefKeys.SHOW_CARDS_IN_TIMELINES, false)) CardViewMode.INDENTED else CardViewMode.NONE, + confirmReblogs = preferences.getBoolean(PrefKeys.CONFIRM_REBLOGS, true), + hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false), + animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) + ) + adapter = TimelineAdapter(dataSource, statusDisplayOptions, this) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_timeline, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + setupSwipeRefreshLayout() + setupRecyclerView() + updateAdapter() + setupTimelinePreferences() + if (statuses.isEmpty()) { + binding.progressBar.show() + bottomLoading = true + sendInitialRequest() + } else { + binding.progressBar.hide() + if (isNeedRefresh) { + onRefresh() + } + } + } + + private fun sendInitialRequest() { + if (kind == Kind.HOME) { + tryCache() + } else { + sendFetchTimelineRequest(null, null, null, FetchEnd.BOTTOM, -1) + } + } + + private fun tryCache() { + // Request timeline from disk to make it quick, then replace it with timeline from + // the server to update it + timelineRepo.getStatuses(null, null, null, LOAD_AT_ONCE, TimelineRequestMode.DISK) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(from(this)) + .subscribe { statuses: List> -> + val mutableStatusResponse = statuses.toMutableList() + filterStatuses(mutableStatusResponse) + if (statuses.size > 1) { + clearPlaceholdersForResponse(mutableStatusResponse) + this.statuses.clear() + this.statuses.addAll(statuses) + updateAdapter() + binding.progressBar.hide() + // Request statuses including current top to refresh all of them + } + updateCurrent() + loadAbove() + } + } + + private fun updateCurrent() { + if (statuses.isEmpty()) { + return + } + val topId = statuses.first { status -> status.isRight() }!!.asRight().id + timelineRepo.getStatuses(topId, null, null, LOAD_AT_ONCE, + TimelineRequestMode.NETWORK) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(from(this)) + .subscribe( + { statuses: List> -> + + initialUpdateFailed = false + // When cached timeline is too old, we would replace it with nothing + if (statuses.isNotEmpty()) { + val mutableStatuses = statuses.toMutableList() + filterStatuses(mutableStatuses) + if (!this.statuses.isEmpty()) { + // clear old cached statuses + val iterator = this.statuses.iterator() + while (iterator.hasNext()) { + val item = iterator.next() + if (item.isRight()) { + val (id1) = item.asRight() + if (id1.length < topId.length || id1 < topId) { + iterator.remove() + } + } else { + val (id1) = item.asLeft() + if (id1.length < topId.length || id1 < topId) { + iterator.remove() + } + } + } + } + this.statuses.addAll(mutableStatuses) + updateAdapter() + } + bottomLoading = false + }, + { t: Throwable? -> + Log.d(TAG, "Failed updating timeline", t) + initialUpdateFailed = true + // Indicate that we are not loading anymore + binding.progressBar.hide() + binding.swipeRefreshLayout.isRefreshing = false + }) + } + + private fun setupTimelinePreferences() { + alwaysShowSensitiveMedia = accountManager.activeAccount!!.alwaysShowSensitiveMedia + alwaysOpenSpoiler = accountManager.activeAccount!!.alwaysOpenSpoiler + if (kind == Kind.HOME) { + val preferences = PreferenceManager.getDefaultSharedPreferences(context) + filterRemoveReplies = !preferences.getBoolean("tabFilterHomeReplies", true) + filterRemoveReblogs = !preferences.getBoolean("tabFilterHomeBoosts", true) + } + reloadFilters(false) + } + + override fun filterIsRelevant(filter: Filter): Boolean { + return filterContextMatchesKind(kind, filter.context) + } + + override fun refreshAfterApplyingFilters() { + fullyRefresh() + } + + private fun setupSwipeRefreshLayout() { + binding.swipeRefreshLayout.isEnabled = isSwipeToRefreshEnabled + binding.swipeRefreshLayout.setOnRefreshListener(this) + binding.swipeRefreshLayout.setColorSchemeResources(R.color.chinwag_green) + } + + private fun setupRecyclerView() { + binding.recyclerView.setAccessibilityDelegateCompat( + ListStatusAccessibilityDelegate(binding.recyclerView, this) + { pos -> statuses.getPairedItemOrNull(pos) } + ) + binding.recyclerView.setHasFixedSize(true) + layoutManager = LinearLayoutManager(context) + binding.recyclerView.layoutManager = layoutManager + val divider = DividerItemDecoration(context, RecyclerView.VERTICAL) + binding.recyclerView.addItemDecoration(divider) + + // CWs are expanded without animation, buttons animate itself, we don't need it basically + (binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false + binding.recyclerView.adapter = adapter + } + + private fun deleteStatusById(id: String) { + for (i in statuses.indices) { + val either = statuses[i] + if (either.isRight() && id == either.asRight().id) { + statuses.remove(either) + updateAdapter() + break + } + } + if (statuses.isEmpty()) { + showEmptyView() + } + } + + private fun showEmptyView() { + binding.statusView.show() + binding.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null) + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + /* This is delayed until onActivityCreated solely because MainActivity.composeButton isn't + * guaranteed to be set until then. */ + scrollListener = if (actionButtonPresent()) { + /* Use a modified scroll listener that both loads more statuses as it goes, and hides + * the follow button on down-scroll. */ + val preferences = PreferenceManager.getDefaultSharedPreferences(context) + hideFab = preferences.getBoolean("fabHide", false) + object : EndlessOnScrollListener(layoutManager) { + override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(view, dx, dy) + val composeButton = (activity as ActionButtonActivity).actionButton + if (composeButton != null) { + if (hideFab) { + if (dy > 0 && composeButton.isShown) { + composeButton.hide() // hides the button if we're scrolling down + } else if (dy < 0 && !composeButton.isShown) { + composeButton.show() // shows it if we are scrolling up + } + } else if (!composeButton.isShown) { + composeButton.show() + } + } + } + + override fun onLoadMore(totalItemsCount: Int, view: RecyclerView) { + this@TimelineFragment.onLoadMore() + } + } + } else { + // Just use the basic scroll listener to load more statuses. + object : EndlessOnScrollListener(layoutManager) { + override fun onLoadMore(totalItemsCount: Int, view: RecyclerView) { + this@TimelineFragment.onLoadMore() + } + } + }.also { + binding.recyclerView.addOnScrollListener(it) + } + + if (!eventRegistered) { + eventHub.events + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(from(this, Lifecycle.Event.ON_DESTROY)) + .subscribe { event: Event? -> + when (event) { + is FavoriteEvent -> handleFavEvent(event) + is ReblogEvent -> handleReblogEvent(event) + is BookmarkEvent -> handleBookmarkEvent(event) + is MuteConversationEvent -> fullyRefresh() + is UnfollowEvent -> { + if (kind == Kind.HOME) { + val id = event.accountId + removeAllByAccountId(id) + } + } + is BlockEvent -> { + if (kind != Kind.USER && kind != Kind.USER_WITH_REPLIES && kind != Kind.USER_PINNED) { + val id = event.accountId + removeAllByAccountId(id) + } + } + is MuteEvent -> { + if (kind != Kind.USER && kind != Kind.USER_WITH_REPLIES && kind != Kind.USER_PINNED) { + val id = event.accountId + removeAllByAccountId(id) + } + } + is DomainMuteEvent -> { + if (kind != Kind.USER && kind != Kind.USER_WITH_REPLIES && kind != Kind.USER_PINNED) { + val instance = event.instance + removeAllByInstance(instance) + } + } + is StatusDeletedEvent -> { + if (kind != Kind.USER && kind != Kind.USER_WITH_REPLIES && kind != Kind.USER_PINNED) { + val id = event.statusId + deleteStatusById(id) + } + } + is StatusComposedEvent -> { + val status = event.status + handleStatusComposeEvent(status) + } + is PreferenceChangedEvent -> { + onPreferenceChanged(event.preferenceKey) + } + } + } + eventRegistered = true + } + } + + override fun onRefresh() { + binding.swipeRefreshLayout.isEnabled = isSwipeToRefreshEnabled + binding.statusView.hide() + isNeedRefresh = false + if (initialUpdateFailed) { + updateCurrent() + } + loadAbove() + } + + private fun loadAbove() { + var firstOrNull: String? = null + var secondOrNull: String? = null + for (i in statuses.indices) { + val status = statuses[i] + if (status.isRight()) { + firstOrNull = status.asRight().id + if (i + 1 < statuses.size && statuses[i + 1].isRight()) { + secondOrNull = statuses[i + 1].asRight().id + } + break + } + } + if (firstOrNull != null) { + sendFetchTimelineRequest(null, firstOrNull, secondOrNull, FetchEnd.TOP, -1) + } else { + sendFetchTimelineRequest(null, null, null, FetchEnd.BOTTOM, -1) + } + } + + override fun onReply(position: Int) { + super.reply(statuses[position].asRight()) + } + + override fun onReblog(reblog: Boolean, position: Int) { + val status = statuses[position].asRight() + timelineCases.reblog(status, reblog) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(from(this)) + .subscribe( + { newStatus: Status -> setRebloggedForStatus(position, newStatus, reblog) } + ) { t: Throwable? -> Log.d(TAG, "Failed to reblog status " + status.id, t) } + } + + private fun setRebloggedForStatus(position: Int, status: Status, reblog: Boolean) { + status.reblogged = reblog + if (status.reblog != null) { + status.reblog.reblogged = reblog + } + val actual = findStatusAndPosition(position, status) ?: return + val newViewData: StatusViewData = StatusViewData.Builder(actual.first) + .setReblogged(reblog) + .createStatusViewData() + statuses.setPairedItem(actual.second!!, newViewData) + updateAdapter() + } + + override fun onFavourite(favourite: Boolean, position: Int) { + val status = statuses[position].asRight() + timelineCases.favourite(status, favourite) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(from(this)) + .subscribe( + { newStatus: Status -> setFavouriteForStatus(position, newStatus, favourite) }, + { t: Throwable? -> Log.d(TAG, "Failed to favourite status " + status.id, t) } + ) + } + + private fun setFavouriteForStatus(position: Int, status: Status, favourite: Boolean) { + status.favourited = favourite + if (status.reblog != null) { + status.reblog.favourited = favourite + } + val actual = findStatusAndPosition(position, status) ?: return + val newViewData: StatusViewData = StatusViewData.Builder(actual.first) + .setFavourited(favourite) + .createStatusViewData() + statuses.setPairedItem(actual.second!!, newViewData) + updateAdapter() + } + + override fun onBookmark(bookmark: Boolean, position: Int) { + val status = statuses[position].asRight() + timelineCases.bookmark(status, bookmark) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(from(this)) + .subscribe( + { newStatus: Status -> setBookmarkForStatus(position, newStatus, bookmark) }, + { t: Throwable? -> Log.d(TAG, "Failed to favourite status " + status.id, t) } + ) + } + + private fun setBookmarkForStatus(position: Int, status: Status, bookmark: Boolean) { + status.bookmarked = bookmark + if (status.reblog != null) { + status.reblog.bookmarked = bookmark + } + val actual = findStatusAndPosition(position, status) ?: return + val newViewData: StatusViewData = StatusViewData.Builder(actual.first) + .setBookmarked(bookmark) + .createStatusViewData() + statuses.setPairedItem(actual.second!!, newViewData) + updateAdapter() + } + + override fun onVoteInPoll(position: Int, choices: List) { + val status = statuses[position].asRight() + val votedPoll = status.actionableStatus.poll!!.votedCopy(choices) + setVoteForPoll(position, status, votedPoll) + timelineCases.voteInPoll(status, choices) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(from(this)) + .subscribe( + { newPoll: Poll -> setVoteForPoll(position, status, newPoll) }, + { t: Throwable? -> Log.d(TAG, "Failed to vote in poll: " + status.id, t) } + ) + } + + private fun setVoteForPoll(position: Int, status: Status, newPoll: Poll) { + val actual = findStatusAndPosition(position, status) ?: return + val newViewData: StatusViewData = StatusViewData.Builder(actual.first) + .setPoll(newPoll) + .createStatusViewData() + statuses.setPairedItem(actual.second!!, newViewData) + updateAdapter() + } + + override fun onMore(view: View, position: Int) { + super.more(statuses[position].asRight(), view, position) + } + + override fun onOpenReblog(position: Int) { + super.openReblog(statuses[position].asRight()) + } + + override fun onExpandedChange(expanded: Boolean, position: Int) { + val newViewData: StatusViewData = StatusViewData.Builder( + statuses.getPairedItem(position) as StatusViewData.Concrete) + .setIsExpanded(expanded).createStatusViewData() + statuses.setPairedItem(position, newViewData) + updateAdapter() + } + + override fun onContentHiddenChange(isShowing: Boolean, position: Int) { + val newViewData: StatusViewData = StatusViewData.Builder( + statuses.getPairedItem(position) as StatusViewData.Concrete) + .setIsShowingSensitiveContent(isShowing).createStatusViewData() + statuses.setPairedItem(position, newViewData) + updateAdapter() + } + + override fun onShowReblogs(position: Int) { + val statusId = statuses[position].asRight().id + val intent = newIntent(requireContext(), AccountListActivity.Type.REBLOGGED, statusId) + (activity as BaseActivity).startActivityWithSlideInAnimation(intent) + } + + override fun onShowFavs(position: Int) { + val statusId = statuses[position].asRight().id + val intent = newIntent(requireContext(), AccountListActivity.Type.FAVOURITED, statusId) + (activity as BaseActivity).startActivityWithSlideInAnimation(intent) + } + + override fun onLoadMore(position: Int) { + //check bounds before accessing list, + if (statuses.size >= position && position > 0) { + val fromStatus = statuses[position - 1].asRightOrNull() + val toStatus = statuses[position + 1].asRightOrNull() + val maxMinusOne = if (statuses.size > position + 1 && statuses[position + 2].isRight()) statuses[position + 1].asRight().id else null + if (fromStatus == null || toStatus == null) { + Log.e(TAG, "Failed to load more at $position, wrong placeholder position") + return + } + sendFetchTimelineRequest(fromStatus.id, toStatus.id, maxMinusOne, + FetchEnd.MIDDLE, position) + val (id1) = statuses[position].asLeft() + val newViewData: StatusViewData = StatusViewData.Placeholder(id1, true) + statuses.setPairedItem(position, newViewData) + updateAdapter() + } else { + Log.e(TAG, "error loading more") + } + } + + override fun onContentCollapsedChange(isCollapsed: Boolean, position: Int) { + if (position < 0 || position >= statuses.size) { + Log.e(TAG, String.format("Tried to access out of bounds status position: %d of %d", position, statuses.size - 1)) + return + } + val status = statuses.getPairedItem(position) + if (status !is StatusViewData.Concrete) { + // Statuses PairedList contains a base type of StatusViewData.Concrete and also doesn't + // check for null values when adding values to it although this doesn't seem to be an issue. + Log.e(TAG, String.format( + "Expected StatusViewData.Concrete, got %s instead at position: %d of %d", + status?.javaClass?.simpleName ?: "", + position, + statuses.size - 1 + )) + return + } + val updatedStatus: StatusViewData = StatusViewData.Builder(status) + .setCollapsed(isCollapsed) + .createStatusViewData() + statuses.setPairedItem(position, updatedStatus) + updateAdapter() + } + + override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) { + val status = statuses.getOrNull(position)?.asRightOrNull() ?: return + super.viewMedia(attachmentIndex, status, view) + } + + override fun onViewThread(position: Int) { + super.viewThread(statuses[position].asRight()) + } + + override fun onViewTag(tag: String) { + if (kind == Kind.TAG && tags.size == 1 && tags.contains(tag)) { + // If already viewing a tag page, then ignore any request to view that tag again. + return + } + super.viewTag(tag) + } + + override fun onViewAccount(id: String) { + if ((kind == Kind.USER || kind == Kind.USER_WITH_REPLIES) && this.id == id) { + /* If already viewing an account page, then any requests to view that account page + * should be ignored. */ + return + } + super.viewAccount(id) + } + + private fun onPreferenceChanged(key: String) { + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) + when (key) { + PrefKeys.FAB_HIDE -> { + hideFab = sharedPreferences.getBoolean(PrefKeys.FAB_HIDE, false) + } + PrefKeys.MEDIA_PREVIEW_ENABLED -> { + val enabled = accountManager.activeAccount!!.mediaPreviewEnabled + val oldMediaPreviewEnabled = adapter.mediaPreviewEnabled + if (enabled != oldMediaPreviewEnabled) { + adapter.mediaPreviewEnabled = enabled + fullyRefresh() + } + } + PrefKeys.TAB_FILTER_HOME_REPLIES -> { + val filter = sharedPreferences.getBoolean(PrefKeys.TAB_FILTER_HOME_REPLIES, true) + val oldRemoveReplies = filterRemoveReplies + filterRemoveReplies = kind == Kind.HOME && !filter + if (adapter.itemCount > 1 && oldRemoveReplies != filterRemoveReplies) { + fullyRefresh() + } + } + PrefKeys.TAB_FILTER_HOME_BOOSTS -> { + val filter = sharedPreferences.getBoolean(PrefKeys.TAB_FILTER_HOME_BOOSTS, true) + val oldRemoveReblogs = filterRemoveReblogs + filterRemoveReblogs = kind == Kind.HOME && !filter + if (adapter.itemCount > 1 && oldRemoveReblogs != filterRemoveReblogs) { + fullyRefresh() + } + } + Filter.HOME, Filter.NOTIFICATIONS, Filter.THREAD, Filter.PUBLIC, Filter.ACCOUNT -> { + if (filterContextMatchesKind(kind, listOf(key))) { + reloadFilters(true) + } + } + PrefKeys.ALWAYS_SHOW_SENSITIVE_MEDIA -> { + //it is ok if only newly loaded statuses are affected, no need to fully refresh + alwaysShowSensitiveMedia = accountManager.activeAccount!!.alwaysShowSensitiveMedia + } + } + } + + public override fun removeItem(position: Int) { + statuses.removeAt(position) + updateAdapter() + } + + private fun removeAllByAccountId(accountId: String) { + // using iterator to safely remove items while iterating + val iterator = statuses.iterator() + while (iterator.hasNext()) { + val status = iterator.next().asRightOrNull() + if (status != null && + (status.account.id == accountId || status.actionableStatus.account.id == accountId)) { + iterator.remove() + } + } + updateAdapter() + } + + private fun removeAllByInstance(instance: String) { + // using iterator to safely remove items while iterating + val iterator = statuses.iterator() + while (iterator.hasNext()) { + val status = iterator.next().asRightOrNull() + if (status != null && LinkHelper.getDomain(status.account.url) == instance) { + iterator.remove() + } + } + updateAdapter() + } + + private fun onLoadMore() { + if (didLoadEverythingBottom || bottomLoading) { + return + } + if (statuses.isEmpty()) { + sendInitialRequest() + return + } + bottomLoading = true + val last = statuses[statuses.size - 1] + val placeholder: Placeholder + if (last!!.isRight()) { + val placeholderId = last.asRight().id.dec() + placeholder = Placeholder(placeholderId) + statuses.add(Left(placeholder)) + } else { + placeholder = last.asLeft() + } + statuses.setPairedItem(statuses.size - 1, + StatusViewData.Placeholder(placeholder.id, true)) + updateAdapter() + + val bottomId: String? = if (kind == Kind.FAVOURITES || kind == Kind.BOOKMARKS) { + nextId + } else { + statuses.lastOrNull { it.isRight() }?.asRight()?.id + } + + sendFetchTimelineRequest(bottomId, null, null, FetchEnd.BOTTOM, -1) + } + + private fun fullyRefresh() { + statuses.clear() + updateAdapter() + bottomLoading = true + sendFetchTimelineRequest(null, null, null, FetchEnd.BOTTOM, -1) + } + + private fun actionButtonPresent(): Boolean { + return kind != Kind.TAG && kind != Kind.FAVOURITES && kind != Kind.BOOKMARKS && + activity is ActionButtonActivity + } + + private fun getFetchCallByTimelineType(fromId: String?, uptoId: String?): Single>> { + val api = mastodonApi + return when (kind) { + Kind.HOME -> api.homeTimeline(fromId, uptoId, LOAD_AT_ONCE) + Kind.PUBLIC_FEDERATED -> api.publicTimeline(null, fromId, uptoId, LOAD_AT_ONCE) + Kind.PUBLIC_LOCAL -> api.publicTimeline(true, fromId, uptoId, LOAD_AT_ONCE) + Kind.TAG -> { + val firstHashtag = tags[0] + val additionalHashtags = tags.subList(1, tags.size) + api.hashtagTimeline(firstHashtag, additionalHashtags, null, fromId, uptoId, LOAD_AT_ONCE) + } + Kind.USER -> api.accountStatuses(id!!, fromId, uptoId, LOAD_AT_ONCE, true, null, null) + Kind.USER_PINNED -> api.accountStatuses(id!!, fromId, uptoId, LOAD_AT_ONCE, null, null, true) + Kind.USER_WITH_REPLIES -> api.accountStatuses(id!!, fromId, uptoId, LOAD_AT_ONCE, null, null, null) + Kind.FAVOURITES -> api.favourites(fromId, uptoId, LOAD_AT_ONCE) + Kind.BOOKMARKS -> api.bookmarks(fromId, uptoId, LOAD_AT_ONCE) + Kind.LIST -> api.listTimeline(id!!, fromId, uptoId, LOAD_AT_ONCE) + else -> api.homeTimeline(fromId, uptoId, LOAD_AT_ONCE) + } + } + + private fun sendFetchTimelineRequest(maxId: String?, sinceId: String?, + sinceIdMinusOne: String?, + fetchEnd: FetchEnd, pos: Int) { + if (isAdded && (fetchEnd == FetchEnd.TOP || fetchEnd == FetchEnd.BOTTOM && maxId == null && binding.progressBar.visibility != View.VISIBLE) && !isSwipeToRefreshEnabled) { + binding.topProgressBar.show() + } + if (kind == Kind.HOME) { + // allow getting old statuses/fallbacks for network only for for bottom loading + val mode = if (fetchEnd == FetchEnd.BOTTOM) { + TimelineRequestMode.ANY + } else { + TimelineRequestMode.NETWORK + } + timelineRepo.getStatuses(maxId, sinceId, sinceIdMinusOne, LOAD_AT_ONCE, mode) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(from(this)) + .subscribe( + { result: List> -> onFetchTimelineSuccess(result.toMutableList(), fetchEnd, pos) }, + { t: Throwable -> onFetchTimelineFailure(t, fetchEnd, pos) } + ) + } else { + getFetchCallByTimelineType(maxId, sinceId) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(from(this)) + .subscribe( + { response: Response> -> + if (response.isSuccessful) { + val newNextId = extractNextId(response) + if (newNextId != null) { + // when we reach the bottom of the list, we won't have a new link. If + // we blindly write `null` here we will start loading from the top + // again. + nextId = newNextId + } + onFetchTimelineSuccess(liftStatusList(response.body()!!).toMutableList(), fetchEnd, pos) + } else { + onFetchTimelineFailure(Exception(response.message()), fetchEnd, pos) + } + } + ) { t: Throwable -> onFetchTimelineFailure(t, fetchEnd, pos) } + } + } + + private fun extractNextId(response: Response<*>): String? { + val linkHeader = response.headers()["Link"] ?: return null + val links = HttpHeaderLink.parse(linkHeader) + val nextHeader = HttpHeaderLink.findByRelationType(links, "next") ?: return null + val nextLink = nextHeader.uri ?: return null + return nextLink.getQueryParameter("max_id") + } + + private fun onFetchTimelineSuccess(statuses: MutableList>, + fetchEnd: FetchEnd, pos: Int) { + + // We filled the hole (or reached the end) if the server returned less statuses than we + // we asked for. + val fullFetch = statuses.size >= LOAD_AT_ONCE + filterStatuses(statuses) + when (fetchEnd) { + FetchEnd.TOP -> { + updateStatuses(statuses, fullFetch) + } + FetchEnd.MIDDLE -> { + replacePlaceholderWithStatuses(statuses, fullFetch, pos) + } + FetchEnd.BOTTOM -> { + if (!this.statuses.isEmpty() + && !this.statuses[this.statuses.size - 1].isRight()) { + this.statuses.removeAt(this.statuses.size - 1) + updateAdapter() + } + if (statuses.isNotEmpty() && !statuses[statuses.size - 1].isRight()) { + // Removing placeholder if it's the last one from the cache + statuses.removeAt(statuses.size - 1) + } + val oldSize = this.statuses.size + if (this.statuses.size > 1) { + addItems(statuses) + } else { + updateStatuses(statuses, fullFetch) + } + if (this.statuses.size == oldSize) { + // This may be a brittle check but seems like it works + // Can we check it using headers somehow? Do all server support them? + didLoadEverythingBottom = true + } + } + } + if (isAdded) { + binding.topProgressBar.hide() + updateBottomLoadingState(fetchEnd) + binding.progressBar.hide() + binding.swipeRefreshLayout.isRefreshing = false + binding.swipeRefreshLayout.isEnabled = true + if (this.statuses.size == 0) { + showEmptyView() + } else { + binding.statusView.hide() + } + } + } + + private fun onFetchTimelineFailure(throwable: Throwable, fetchEnd: FetchEnd, position: Int) { + if (isAdded) { + binding.swipeRefreshLayout.isRefreshing = false + binding.topProgressBar.hide() + if (fetchEnd == FetchEnd.MIDDLE && !statuses[position].isRight()) { + var placeholder = statuses[position].asLeftOrNull() + val newViewData: StatusViewData + if (placeholder == null) { + val (id1) = statuses[position - 1].asRight() + val newId = id1.dec() + placeholder = Placeholder(newId) + } + newViewData = StatusViewData.Placeholder(placeholder.id, false) + statuses.setPairedItem(position, newViewData) + updateAdapter() + } else if (statuses.isEmpty()) { + binding.swipeRefreshLayout.isEnabled = false + binding.statusView.visibility = View.VISIBLE + if (throwable is IOException) { + binding.statusView.setup(R.drawable.elephant_offline, R.string.error_network) { + binding.progressBar.visibility = View.VISIBLE + onRefresh() + } + } else { + binding.statusView.setup(R.drawable.elephant_error, R.string.error_generic) { + binding.progressBar.visibility = View.VISIBLE + onRefresh() + } + } + } + Log.e(TAG, "Fetch Failure: " + throwable.message) + updateBottomLoadingState(fetchEnd) + binding.progressBar.hide() + } + } + + private fun updateBottomLoadingState(fetchEnd: FetchEnd) { + if (fetchEnd == FetchEnd.BOTTOM) { + bottomLoading = false + } + } + + private fun filterStatuses(statuses: MutableList>) { + val it = statuses.iterator() + while (it.hasNext()) { + val status = it.next().asRightOrNull() + if (status != null + && (status.inReplyToId != null && filterRemoveReplies + || status.reblog != null && filterRemoveReblogs + || shouldFilterStatus(status.actionableStatus))) { + it.remove() + } + } + } + + private fun updateStatuses(newStatuses: MutableList>, fullFetch: Boolean) { + if (newStatuses.isEmpty()) { + updateAdapter() + return + } + if (statuses.isEmpty()) { + statuses.addAll(newStatuses) + } else { + val lastOfNew = newStatuses[newStatuses.size - 1] + val index = statuses.indexOf(lastOfNew) + if (index >= 0) { + statuses.subList(0, index).clear() + } + val newIndex = newStatuses.indexOf(statuses[0]) + if (newIndex == -1) { + if (index == -1 && fullFetch) { + val placeholderId = newStatuses.last { status -> status.isRight() }.asRight().id.inc() + newStatuses.add(Left(Placeholder(placeholderId))) + } + statuses.addAll(0, newStatuses) + } else { + statuses.addAll(0, newStatuses.subList(0, newIndex)) + } + } + // Remove all consecutive placeholders + removeConsecutivePlaceholders() + updateAdapter() + } + + private fun removeConsecutivePlaceholders() { + for (i in 0 until statuses.size - 1) { + if (statuses[i].isLeft() && statuses[i + 1].isLeft()) { + statuses.removeAt(i) + } + } + } + + private fun addItems(newStatuses: List?>) { + if (newStatuses.isEmpty()) { + return + } + val last = statuses.last { status -> + status.isRight() + } + + // I was about to replace findStatus with indexOf but it is incorrect to compare value + // types by ID anyway and we should change equals() for Status, I think, so this makes sense + if (last != null && !newStatuses.contains(last)) { + statuses.addAll(newStatuses) + removeConsecutivePlaceholders() + updateAdapter() + } + } + + /** + * For certain requests we don't want to see placeholders, they will be removed some other way + */ + private fun clearPlaceholdersForResponse(statuses: MutableList>) { + statuses.removeAll{ status -> status.isLeft() } + } + + private fun replacePlaceholderWithStatuses(newStatuses: MutableList>, + fullFetch: Boolean, pos: Int) { + val placeholder = statuses[pos] + if (placeholder.isLeft()) { + statuses.removeAt(pos) + } + if (newStatuses.isEmpty()) { + updateAdapter() + return + } + if (fullFetch) { + newStatuses.add(placeholder) + } + statuses.addAll(pos, newStatuses) + removeConsecutivePlaceholders() + updateAdapter() + } + + private fun findStatusOrReblogPositionById(statusId: String): Int { + return statuses.indexOfFirst { either -> + val status = either.asRightOrNull() + status != null && + (statusId == status.id || + (status.reblog != null && statusId == status.reblog.id)) + } + } + + private val statusLifter: Function1> = { value -> Right(value) } + + private fun findStatusAndPosition(position: Int, status: Status): Pair? { + val statusToUpdate: StatusViewData.Concrete + val positionToUpdate: Int + val someOldViewData = statuses.getPairedItem(position) + + // Unlikely, but data could change between the request and response + if (someOldViewData is StatusViewData.Placeholder || + (someOldViewData as StatusViewData.Concrete).id != status.id) { + // try to find the status we need to update + val foundPos = statuses.indexOf(Right(status)) + if (foundPos < 0) return null // okay, it's hopeless, give up + statusToUpdate = statuses.getPairedItem(foundPos) as StatusViewData.Concrete + positionToUpdate = position + } else { + statusToUpdate = someOldViewData + positionToUpdate = position + } + return Pair(statusToUpdate, positionToUpdate) + } + + private fun handleReblogEvent(reblogEvent: ReblogEvent) { + val pos = findStatusOrReblogPositionById(reblogEvent.statusId) + if (pos < 0) return + val status = statuses[pos].asRight() + setRebloggedForStatus(pos, status, reblogEvent.reblog) + } + + private fun handleFavEvent(favEvent: FavoriteEvent) { + val pos = findStatusOrReblogPositionById(favEvent.statusId) + if (pos < 0) return + val status = statuses[pos].asRight() + setFavouriteForStatus(pos, status, favEvent.favourite) + } + + private fun handleBookmarkEvent(bookmarkEvent: BookmarkEvent) { + val pos = findStatusOrReblogPositionById(bookmarkEvent.statusId) + if (pos < 0) return + val status = statuses[pos].asRight() + setBookmarkForStatus(pos, status, bookmarkEvent.bookmark) + } + + private fun handleStatusComposeEvent(status: Status) { + when (kind) { + Kind.HOME, Kind.PUBLIC_FEDERATED, Kind.PUBLIC_LOCAL -> onRefresh() + Kind.USER, Kind.USER_WITH_REPLIES -> if (status.account.id == id) { + onRefresh() + } else { + return + } + Kind.TAG, Kind.FAVOURITES, Kind.LIST, Kind.BOOKMARKS, Kind.USER_PINNED -> return + } + } + + private fun liftStatusList(list: List): List> { + return list.map(statusLifter) + } + + private fun updateAdapter() { + differ.submitList(statuses.pairedCopy) + } + + private val listUpdateCallback: ListUpdateCallback = object : ListUpdateCallback { + override fun onInserted(position: Int, count: Int) { + if (isAdded) { + adapter.notifyItemRangeInserted(position, count) + val context = context + // scroll up when new items at the top are loaded while being in the first position + // https://github.com/tuskyapp/Tusky/pull/1905#issuecomment-677819724 + if (position == 0 && context != null && adapter.itemCount != count) { + if (isSwipeToRefreshEnabled) { + binding.recyclerView.scrollBy(0, Utils.dpToPx(context, -30)) + } else binding.recyclerView.scrollToPosition(0) + } + } + } + + override fun onRemoved(position: Int, count: Int) { + adapter.notifyItemRangeRemoved(position, count) + } + + override fun onMoved(fromPosition: Int, toPosition: Int) { + adapter.notifyItemMoved(fromPosition, toPosition) + } + + override fun onChanged(position: Int, count: Int, payload: Any?) { + adapter.notifyItemRangeChanged(position, count, payload) + } + } + private val differ = AsyncListDiffer(listUpdateCallback, + AsyncDifferConfig.Builder(diffCallback).build()) + + private val dataSource: TimelineAdapter.AdapterDataSource = object : TimelineAdapter.AdapterDataSource { + override fun getItemCount(): Int { + return differ.currentList.size + } + + override fun getItemAt(pos: Int): StatusViewData { + return differ.currentList[pos] + } + } + + private var talkBackWasEnabled = false + + override fun onResume() { + super.onResume() + val a11yManager = ContextCompat.getSystemService(requireContext(), AccessibilityManager::class.java) + + val wasEnabled = talkBackWasEnabled + talkBackWasEnabled = a11yManager?.isEnabled == true + Log.d(TAG, "talkback was enabled: $wasEnabled, now $talkBackWasEnabled") + if (talkBackWasEnabled && !wasEnabled) { + adapter.notifyDataSetChanged() + } + startUpdateTimestamp() + } + + /** + * Start to update adapter every minute to refresh timestamp + * If setting absoluteTimeView is false + * Auto dispose observable on pause + */ + private fun startUpdateTimestamp() { + val preferences = PreferenceManager.getDefaultSharedPreferences(activity) + val useAbsoluteTime = preferences.getBoolean(PrefKeys.ABSOLUTE_TIME_VIEW, false) + if (!useAbsoluteTime) { + Observable.interval(1, TimeUnit.MINUTES) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(from(this, Lifecycle.Event.ON_PAUSE)) + .subscribe { updateAdapter() } + } + } + + override fun onReselect() { + if (isAdded) { + layoutManager!!.scrollToPosition(0) + binding.recyclerView.stopScroll() + scrollListener!!.reset() + } + } + + override fun refreshContent() { + if (isAdded) { + onRefresh() + } else { + isNeedRefresh = true + } + } + + enum class Kind { + HOME, PUBLIC_LOCAL, PUBLIC_FEDERATED, TAG, USER, USER_PINNED, USER_WITH_REPLIES, FAVOURITES, LIST, BOOKMARKS + } + + private enum class FetchEnd { + TOP, BOTTOM, MIDDLE + } + + companion object { + private const val TAG = "TimelineF" // logging tag + private const val KIND_ARG = "kind" + private const val ID_ARG = "id" + private const val HASHTAGS_ARG = "hashtags" + private const val ARG_ENABLE_SWIPE_TO_REFRESH = "enableSwipeToRefresh" + private const val LOAD_AT_ONCE = 30 + + fun newInstance(kind: Kind, hashtagOrId: String? = null, enableSwipeToRefresh: Boolean = true): TimelineFragment { + val fragment = TimelineFragment() + val arguments = Bundle(3) + arguments.putString(KIND_ARG, kind.name) + arguments.putString(ID_ARG, hashtagOrId) + arguments.putBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, enableSwipeToRefresh) + fragment.arguments = arguments + return fragment + } + + @JvmStatic + fun newHashtagInstance(hashtags: List): TimelineFragment { + val fragment = TimelineFragment() + val arguments = Bundle(3) + arguments.putString(KIND_ARG, Kind.TAG.name) + arguments.putStringArrayList(HASHTAGS_ARG, ArrayList(hashtags)) + arguments.putBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, true) + fragment.arguments = arguments + return fragment + } + + private fun filterContextMatchesKind(kind: Kind?, filterContext: List): Boolean { + // home, notifications, public, thread + return when (kind) { + Kind.HOME, Kind.LIST -> filterContext.contains(Filter.HOME) + Kind.PUBLIC_FEDERATED, Kind.PUBLIC_LOCAL, Kind.TAG -> filterContext.contains(Filter.PUBLIC) + Kind.FAVOURITES -> filterContext.contains(Filter.PUBLIC) || filterContext.contains(Filter.NOTIFICATIONS) + Kind.USER, Kind.USER_WITH_REPLIES, Kind.USER_PINNED -> filterContext.contains(Filter.ACCOUNT) + else -> false + } + } + + private val diffCallback: DiffUtil.ItemCallback = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: StatusViewData, newItem: StatusViewData): Boolean { + return oldItem.viewDataId == newItem.viewDataId + } + + override fun areContentsTheSame(oldItem: StatusViewData, newItem: StatusViewData): Boolean { + return false // Items are different always. It allows to refresh timestamp on every view holder update + } + + override fun getChangePayload(oldItem: StatusViewData, newItem: StatusViewData): Any? { + return if (oldItem.deepEquals(newItem)) { + // If items are equal - update timestamp only + listOf(StatusBaseViewHolder.Key.KEY_CREATED) + } else // If items are different - update the whole view holder + null + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewImageFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewImageFragment.kt index fc4dfcb9..c68cfb5f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewImageFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewImageFragment.kt @@ -32,13 +32,12 @@ import com.bumptech.glide.load.engine.GlideException import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.target.Target import com.github.chrisbanes.photoview.PhotoViewAttacher -import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.ViewMediaActivity +import com.keylesspalace.tusky.databinding.FragmentViewImageBinding import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.visible import io.reactivex.subjects.BehaviorSubject -import kotlinx.android.synthetic.main.activity_view_media.* -import kotlinx.android.synthetic.main.fragment_view_image.* import kotlin.math.abs class ViewImageFragment : ViewMediaFragment() { @@ -48,6 +47,9 @@ class ViewImageFragment : ViewMediaFragment() { fun onPhotoTap() } + private var _binding: FragmentViewImageBinding? = null + private val binding get() = _binding!! + private lateinit var attacher: PhotoViewAttacher private lateinit var photoActionsListener: PhotoActionsListener private lateinit var toolbar: View @@ -71,18 +73,19 @@ class ViewImageFragment : ViewMediaFragment() { description: String?, showingDescription: Boolean ) { - photoView.transitionName = url - mediaDescription.text = description - captionSheet.visible(showingDescription) + binding.photoView.transitionName = url + binding.mediaDescription.text = description + binding.captionSheet.visible(showingDescription) startedTransition = false - loadImageFromNetwork(url, previewUrl, photoView) + loadImageFromNetwork(url, previewUrl, binding.photoView) } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - toolbar = requireActivity().toolbar + toolbar = (requireActivity() as ViewMediaActivity).toolbar this.transition = BehaviorSubject.create() - return inflater.inflate(R.layout.fragment_view_image, container, false) + _binding = FragmentViewImageBinding.inflate(inflater, container, false) + return binding.root } @SuppressLint("ClickableViewAccessibility") @@ -105,7 +108,7 @@ class ViewImageFragment : ViewMediaFragment() { } } - attacher = PhotoViewAttacher(photoView).apply { + attacher = PhotoViewAttacher(binding.photoView).apply { // This prevents conflicts with ViewPager setAllowParentInterceptOnEdge(true) @@ -127,7 +130,7 @@ class ViewImageFragment : ViewMediaFragment() { var lastY = 0f - photoView.setOnTouchListener { v, event -> + binding.photoView.setOnTouchListener { v, event -> // This part is for scaling/translating on vertical move. // We use raw coordinates to get the correct ones during scaling @@ -140,11 +143,11 @@ class ViewImageFragment : ViewMediaFragment() { val diff = event.rawY - lastY // This code is to prevent transformations during page scrolling // If we are already translating or we reached the threshold, then transform. - if (photoView.translationY != 0f || abs(diff) > 40) { - photoView.translationY += (diff) - val scale = (-abs(photoView.translationY) / 720 + 1).coerceAtLeast(0.5f) - photoView.scaleY = scale - photoView.scaleX = scale + if (binding.photoView.translationY != 0f || abs(diff) > 40) { + binding.photoView.translationY += (diff) + val scale = (-abs(binding.photoView.translationY) / 720 + 1).coerceAtLeast(0.5f) + binding.photoView.scaleY = scale + binding.photoView.scaleX = scale lastY = event.rawY return@setOnTouchListener true } @@ -158,13 +161,13 @@ class ViewImageFragment : ViewMediaFragment() { } private fun onGestureEnd() { - if (photoView == null) { + if (_binding == null) { return } - if (abs(photoView.translationY) > 180) { + if (abs(binding.photoView.translationY) > 180) { photoActionsListener.onDismiss() } else { - photoView.animate().translationY(0f).scaleX(1f).scaleY(1f).start() + binding.photoView.animate().translationY(0f).scaleX(1f).scaleY(1f).start() } } @@ -173,15 +176,17 @@ class ViewImageFragment : ViewMediaFragment() { } override fun onToolbarVisibilityChange(visible: Boolean) { - if (photoView == null || !userVisibleHint || captionSheet == null) { + if (_binding == null || !userVisibleHint ) { return } isDescriptionVisible = showingDescription && visible val alpha = if (isDescriptionVisible) 1.0f else 0.0f - captionSheet.animate().alpha(alpha) + binding.captionSheet.animate().alpha(alpha) .setListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { - captionSheet?.visible(isDescriptionVisible) + if (_binding != null) { + binding.captionSheet.visible(isDescriptionVisible) + } animation.removeListener(this) } }) @@ -189,8 +194,9 @@ class ViewImageFragment : ViewMediaFragment() { } override fun onDestroyView() { - Glide.with(this).clear(photoView) + Glide.with(this).clear(binding.photoView) transition.onComplete() + _binding = null super.onDestroyView() } @@ -253,7 +259,7 @@ class ViewImageFragment : ViewMediaFragment() { photoActionsListener.onBringUp() } // Hide progress bar only on fail request from internet - if (!isCacheRequest) progressBar?.hide() + if (!isCacheRequest && _binding != null) binding.progressBar.hide() // We don't want to overwrite preview with null when main image fails to load return !isCacheRequest } @@ -261,14 +267,16 @@ class ViewImageFragment : ViewMediaFragment() { @SuppressLint("CheckResult") override fun onResourceReady(resource: Drawable, model: Any, target: Target, dataSource: DataSource, isFirstResource: Boolean): Boolean { - progressBar?.hide() // Always hide the progress bar on success + if (_binding != null) { + binding.progressBar.hide() // Always hide the progress bar on success + } if (!startedTransition || !shouldStartTransition) { // Set this right away so that we don't have to concurrent post() requests startedTransition = true // post() because load() replaces image with null. Sometimes after we set // the thumbnail. - photoView.post { + binding.photoView.post { target.onResourceReady(resource, null) if (shouldStartTransition) photoActionsListener.onBringUp() } 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 33d8f192..a0912837 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt @@ -26,16 +26,18 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.MediaController -import com.keylesspalace.tusky.R 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 kotlinx.android.synthetic.main.activity_view_media.* -import kotlinx.android.synthetic.main.fragment_view_video.* class ViewVideoFragment : ViewMediaFragment() { + + private var _binding: FragmentViewVideoBinding? = null + private val binding get() = _binding!! + private lateinit var toolbar: View private val handler = Handler(Looper.getMainLooper()) private val hideToolbar = Runnable { @@ -52,7 +54,7 @@ class ViewVideoFragment : ViewMediaFragment() { override fun setUserVisibleHint(isVisibleToUser: Boolean) { // Start/pause/resume video playback as fragment is shown/hidden super.setUserVisibleHint(isVisibleToUser) - if (videoView == null) { + if (_binding == null) { return } @@ -60,10 +62,10 @@ class ViewVideoFragment : ViewMediaFragment() { if (mediaActivity.isToolbarVisible) { handler.postDelayed(hideToolbar, TOOLBAR_HIDE_DELAY_MS) } - videoView.start() + binding.videoView.start() } else { handler.removeCallbacks(hideToolbar) - videoView.pause() + binding.videoView.pause() mediaController.hide() } } @@ -75,11 +77,11 @@ class ViewVideoFragment : ViewMediaFragment() { description: String?, showingDescription: Boolean ) { - mediaDescription.text = description - mediaDescription.visible(showingDescription) + binding.mediaDescription.text = description + binding.mediaDescription.visible(showingDescription) - videoView.transitionName = url - videoView.setVideoPath(url) + binding.videoView.transitionName = url + binding.videoView.setVideoPath(url) mediaController = object : MediaController(mediaActivity) { override fun show(timeout: Int) { // We're doing manual auto-close management. @@ -100,10 +102,10 @@ class ViewVideoFragment : ViewMediaFragment() { } } - mediaController.setMediaPlayer(videoView) - videoView.setMediaController(mediaController) - videoView.requestFocus() - videoView.setPlayPauseListener(object: ExposedPlayPauseVideoView.PlayPauseListener { + mediaController.setMediaPlayer(binding.videoView) + binding.videoView.setMediaController(mediaController) + binding.videoView.requestFocus() + binding.videoView.setPlayPauseListener(object: ExposedPlayPauseVideoView.PlayPauseListener { override fun onPause() { handler.removeCallbacks(hideToolbar) } @@ -117,31 +119,31 @@ class ViewVideoFragment : ViewMediaFragment() { } } }) - videoView.setOnPreparedListener { mp -> - val containerWidth = videoContainer.measuredWidth.toFloat() - val containerHeight = videoContainer.measuredHeight.toFloat() + binding.videoView.setOnPreparedListener { mp -> + val containerWidth = binding.videoContainer.measuredWidth.toFloat() + val containerHeight = binding.videoContainer.measuredHeight.toFloat() val videoWidth = mp.videoWidth.toFloat() val videoHeight = mp.videoHeight.toFloat() if(containerWidth/containerHeight > videoWidth/videoHeight) { - videoView.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT - videoView.layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT + binding.videoView.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT + binding.videoView.layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT } else { - videoView.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT - videoView.layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT + binding.videoView.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT + binding.videoView.layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT } // Wait until the media is loaded before accepting taps as we don't want toolbar to // be hidden until then. - videoView.setOnTouchListener { _, _ -> + binding.videoView.setOnTouchListener { _, _ -> mediaActivity.onPhotoTap() false } - progressBar.hide() + binding.progressBar.hide() mp.isLooping = true if (requireArguments().getBoolean(ARG_START_POSTPONED_TRANSITION)) { - videoView.start() + binding.videoView.start() } } @@ -155,9 +157,10 @@ class ViewVideoFragment : ViewMediaFragment() { } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - toolbar = requireActivity().toolbar mediaActivity = activity as ViewMediaActivity - return inflater.inflate(R.layout.fragment_view_video, container, false) + toolbar = mediaActivity.toolbar + _binding = FragmentViewVideoBinding.inflate(inflater, container, false) + return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -174,7 +177,7 @@ class ViewVideoFragment : ViewMediaFragment() { } override fun onToolbarVisibilityChange(visible: Boolean) { - if (videoView == null || mediaDescription == null || !userVisibleHint) { + if (_binding == null || !userVisibleHint) { return } @@ -182,20 +185,22 @@ class ViewVideoFragment : ViewMediaFragment() { val alpha = if (isDescriptionVisible) 1.0f else 0.0f if (isDescriptionVisible) { // If to be visible, need to make visible immediately and animate alpha - mediaDescription.alpha = 0.0f - mediaDescription.visible(isDescriptionVisible) + binding.mediaDescription.alpha = 0.0f + binding.mediaDescription.visible(isDescriptionVisible) } - mediaDescription.animate().alpha(alpha) + binding.mediaDescription.animate().alpha(alpha) .setListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { - mediaDescription?.visible(isDescriptionVisible) + if (_binding != null) { + binding.mediaDescription.visible(isDescriptionVisible) + } animation.removeListener(this) } }) .start() - if (visible && videoView.isPlaying && !isAudio) { + if (visible && binding.videoView.isPlaying && !isAudio) { hideToolbarAfterDelay(TOOLBAR_HIDE_DELAY_MS) } else { handler.removeCallbacks(hideToolbar) @@ -204,4 +209,9 @@ class ViewVideoFragment : ViewMediaFragment() { override fun onTransitionEnd() { } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt b/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt index 8cf2b688..6e79f075 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt @@ -33,7 +33,7 @@ interface TimelineCases { fun reblog(status: Status, reblog: Boolean): Single fun favourite(status: Status, favourite: Boolean): Single fun bookmark(status: Status, bookmark: Boolean): Single - fun mute(id: String, notifications: Boolean, duration: Int) + fun mute(id: String, notifications: Boolean, duration: Int?) fun block(id: String) fun delete(id: String): Single fun pin(status: Status, pin: Boolean) @@ -104,7 +104,7 @@ class TimelineCasesImpl( } } - override fun mute(id: String, notifications: Boolean, duration: Int) { + override fun mute(id: String, notifications: Boolean, duration: Int?) { mastodonApi.muteAccount(id, notifications, duration) .subscribe({ eventHub.dispatch(MuteEvent(id)) diff --git a/app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt b/app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt index 945c55d3..b3e12aeb 100644 --- a/app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt +++ b/app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt @@ -298,7 +298,7 @@ fun Account.toEntity(accountId: Long, gson: Gson): TimelineAccountEntity { timelineUserId = accountId, localUsername = localUsername, username = username, - displayName = displayName.orEmpty(), + displayName = name, url = url, avatar = avatar, emojis = gson.toJson(emojis), diff --git a/app/src/main/java/com/keylesspalace/tusky/service/SendTootService.kt b/app/src/main/java/com/keylesspalace/tusky/service/SendTootService.kt index e427cf43..dff91bd1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/service/SendTootService.kt +++ b/app/src/main/java/com/keylesspalace/tusky/service/SendTootService.kt @@ -28,7 +28,7 @@ import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.SaveTootHelper import dagger.android.AndroidInjection -import kotlinx.android.parcel.Parcelize +import kotlinx.parcelize.Parcelize import retrofit2.Call import retrofit2.Callback import retrofit2.Response diff --git a/app/src/main/java/com/keylesspalace/tusky/util/BindingViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/util/BindingHolder.kt similarity index 82% rename from app/src/main/java/com/keylesspalace/tusky/util/BindingViewHolder.kt rename to app/src/main/java/com/keylesspalace/tusky/util/BindingHolder.kt index 14aee81b..a7a4c972 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/BindingViewHolder.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/BindingHolder.kt @@ -3,6 +3,6 @@ package com.keylesspalace.tusky.util import androidx.recyclerview.widget.RecyclerView import androidx.viewbinding.ViewBinding -class BindingViewHolder( +class BindingHolder( val binding: T ) : RecyclerView.ViewHolder(binding.root) diff --git a/app/src/main/java/com/keylesspalace/tusky/util/EmojiCompatFont.kt b/app/src/main/java/com/keylesspalace/tusky/util/EmojiCompatFont.kt index 4697a1e9..d0a0e443 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/EmojiCompatFont.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/EmojiCompatFont.kt @@ -256,27 +256,27 @@ class EmojiCompatFont( private const val CHUNK_SIZE = 4096L // The system font gets some special behavior... - private val SYSTEM_DEFAULT = EmojiCompatFont("system-default", + val SYSTEM_DEFAULT = EmojiCompatFont("system-default", "System Default", R.string.caption_systememoji, R.drawable.ic_emoji_34dp, "", "0") - private val BLOBMOJI = EmojiCompatFont("Blobmoji", + val BLOBMOJI = EmojiCompatFont("Blobmoji", "Blobmoji", R.string.caption_blobmoji, R.drawable.ic_blobmoji, "https://tusky.app/hosted/emoji/BlobmojiCompat.ttf", "12.0.0" ) - private val TWEMOJI = EmojiCompatFont("Twemoji", + val TWEMOJI = EmojiCompatFont("Twemoji", "Twemoji", R.string.caption_twemoji, R.drawable.ic_twemoji, "https://tusky.app/hosted/emoji/TwemojiCompat.ttf", "12.0.0" ) - private val NOTOEMOJI = EmojiCompatFont("NotoEmoji", + val NOTOEMOJI = EmojiCompatFont("NotoEmoji", "Noto Emoji", R.string.caption_notoemoji, R.drawable.ic_notoemoji, diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ListStatusAccessibilityDelegate.kt b/app/src/main/java/com/keylesspalace/tusky/util/ListStatusAccessibilityDelegate.kt index 8594dfc6..859162da 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/ListStatusAccessibilityDelegate.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/ListStatusAccessibilityDelegate.kt @@ -22,7 +22,7 @@ import com.keylesspalace.tusky.viewdata.StatusViewData import kotlin.math.min // Not using lambdas because there's boxing of int then -interface StatusProvider { +fun interface StatusProvider { fun getStatus(pos: Int): StatusViewData? } diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ViewBindingExtensions.kt b/app/src/main/java/com/keylesspalace/tusky/util/ViewBindingExtensions.kt new file mode 100644 index 00000000..5fa80fcd --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/ViewBindingExtensions.kt @@ -0,0 +1,67 @@ +package com.keylesspalace.tusky.util + +import android.view.LayoutInflater +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.viewbinding.ViewBinding +import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KProperty + +/** + * https://medium.com/@Zhuinden/simple-one-liner-viewbinding-in-fragments-and-activities-with-kotlin-961430c6c07c + */ + +inline fun AppCompatActivity.viewBinding( + crossinline bindingInflater: (LayoutInflater) -> T +) = lazy(LazyThreadSafetyMode.NONE) { + bindingInflater(layoutInflater) +} + +class FragmentViewBindingDelegate( + val fragment: Fragment, + val viewBindingFactory: (View) -> T +) : ReadOnlyProperty { + private var binding: T? = null + + init { + fragment.lifecycle.addObserver( + object : DefaultLifecycleObserver { + override fun onCreate(owner: LifecycleOwner) { + fragment.viewLifecycleOwnerLiveData.observe( + fragment, + { t -> + t?.lifecycle?.addObserver( + object : DefaultLifecycleObserver { + override fun onDestroy(owner: LifecycleOwner) { + binding = null + } + } + ) + } + ) + } + } + ) + } + + override fun getValue(thisRef: Fragment, property: KProperty<*>): T { + val binding = binding + if (binding != null) { + return binding + } + + val lifecycle = fragment.viewLifecycleOwner.lifecycle + if (!lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) { + throw IllegalStateException("Should not attempt to get bindings when Fragment views are destroyed.") + } + + return viewBindingFactory(thisRef.requireView()).also { this@FragmentViewBindingDelegate.binding = it } + } +} + +fun Fragment.viewBinding(viewBindingFactory: (View) -> T) = + FragmentViewBindingDelegate(this, viewBindingFactory) diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java b/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java index ffe64a14..2e8e67ef 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java @@ -52,7 +52,7 @@ public final class ViewDataUtils { .setSensitive(visibleStatus.getSensitive()) .setIsShowingSensitiveContent(alwaysShowSensitiveMedia || !visibleStatus.getSensitive()) .setSpoilerText(visibleStatus.getSpoilerText()) - .setRebloggedByUsername(status.getReblog() == null ? null : status.getAccount().getDisplayName()) + .setRebloggedByUsername(status.getReblog() == null ? null : status.getAccount().getName()) .setUserFullName(visibleStatus.getAccount().getName()) .setVisibility(visibleStatus.getVisibility()) .setSenderId(visibleStatus.getAccount().getId()) diff --git a/app/src/main/java/com/keylesspalace/tusky/view/BackgroundMessageView.kt b/app/src/main/java/com/keylesspalace/tusky/view/BackgroundMessageView.kt index 4789ac3c..32a7d6b3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/view/BackgroundMessageView.kt +++ b/app/src/main/java/com/keylesspalace/tusky/view/BackgroundMessageView.kt @@ -3,14 +3,14 @@ package com.keylesspalace.tusky.view import android.content.Context import android.util.AttributeSet import android.view.Gravity +import android.view.LayoutInflater import android.view.View import android.widget.LinearLayout import androidx.annotation.DrawableRes import androidx.annotation.StringRes import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ViewBackgroundMessageBinding import com.keylesspalace.tusky.util.visible -import kotlinx.android.synthetic.main.view_background_message.view.* - /** * This view is used for screens with downloadable content which may fail. @@ -22,8 +22,9 @@ class BackgroundMessageView @JvmOverloads constructor( defStyleAttr: Int = 0 ) : LinearLayout(context, attrs, defStyleAttr) { + private val binding = ViewBackgroundMessageBinding.inflate(LayoutInflater.from(context), this) + init { - View.inflate(context, R.layout.view_background_message, this) gravity = Gravity.CENTER_HORIZONTAL orientation = VERTICAL @@ -36,11 +37,14 @@ class BackgroundMessageView @JvmOverloads constructor( * Setup image, message and button. * If [clickListener] is `null` then the button will be hidden. */ - fun setup(@DrawableRes imageRes: Int, @StringRes messageRes: Int, - clickListener: ((v: View) -> Unit)? = null) { - messageTextView.setText(messageRes) - imageView.setImageResource(imageRes) - button.setOnClickListener(clickListener) - button.visible(clickListener != null) + fun setup( + @DrawableRes imageRes: Int, + @StringRes messageRes: Int, + clickListener: ((v: View) -> Unit)? = null + ) { + binding.messageTextView.setText(messageRes) + binding.imageView.setImageResource(imageRes) + binding.button.setOnClickListener(clickListener) + binding.button.visible(clickListener != null) } } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/view/LicenseCard.kt b/app/src/main/java/com/keylesspalace/tusky/view/LicenseCard.kt index 2c73cd54..ad9ae52c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/view/LicenseCard.kt +++ b/app/src/main/java/com/keylesspalace/tusky/view/LicenseCard.kt @@ -17,12 +17,13 @@ package com.keylesspalace.tusky.view import android.content.Context import android.util.AttributeSet +import android.view.LayoutInflater import com.google.android.material.card.MaterialCardView import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.CardLicenseBinding import com.keylesspalace.tusky.util.LinkHelper import com.keylesspalace.tusky.util.ThemeUtils import com.keylesspalace.tusky.util.hide -import kotlinx.android.synthetic.main.card_license.view.* class LicenseCard @JvmOverloads constructor( @@ -32,7 +33,7 @@ class LicenseCard ) : MaterialCardView(context, attrs, defStyleAttr) { init { - inflate(context, R.layout.card_license, this) + val binding = CardLicenseBinding.inflate(LayoutInflater.from(context), this) setCardBackgroundColor(ThemeUtils.getColor(context, R.attr.colorSurface)) @@ -43,12 +44,12 @@ class LicenseCard val link: String? = a.getString(R.styleable.LicenseCard_link) a.recycle() - licenseCardName.text = name - licenseCardLicense.text = license + binding.licenseCardName.text = name + binding.licenseCardLicense.text = license if(link.isNullOrBlank()) { - licenseCardLink.hide() + binding.licenseCardLink.hide() } else { - licenseCardLink.text = link + binding.licenseCardLink.text = link setOnClickListener { LinkHelper.openLink(link, context) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/view/MuteAccountDialog.kt b/app/src/main/java/com/keylesspalace/tusky/view/MuteAccountDialog.kt index 435e2450..022927e6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/view/MuteAccountDialog.kt +++ b/app/src/main/java/com/keylesspalace/tusky/view/MuteAccountDialog.kt @@ -3,29 +3,33 @@ package com.keylesspalace.tusky.view import android.app.Activity -import android.widget.CheckBox -import android.widget.Spinner -import android.widget.TextView import androidx.appcompat.app.AlertDialog import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.DialogMuteAccountBinding fun showMuteAccountDialog( activity: Activity, accountUsername: String, - onOk: (notifications: Boolean, duration: Int) -> Unit + onOk: (notifications: Boolean, duration: Int?) -> Unit ) { - val view = activity.layoutInflater.inflate(R.layout.dialog_mute_account, null) - (view.findViewById(R.id.warning) as TextView).text = - activity.getString(R.string.dialog_mute_warning, accountUsername) - val checkbox: CheckBox = view.findViewById(R.id.checkbox) - checkbox.isChecked = true + val binding = DialogMuteAccountBinding.inflate(activity.layoutInflater) + binding.warning.text = activity.getString(R.string.dialog_mute_warning, accountUsername) + binding.checkbox.isChecked = true AlertDialog.Builder(activity) - .setView(view) + .setView(binding.root) .setPositiveButton(android.R.string.ok) { _, _ -> - val spinner: Spinner = view.findViewById(R.id.duration) val durationValues = activity.resources.getIntArray(R.array.mute_duration_values) - onOk(checkbox.isChecked, durationValues[spinner.selectedItemPosition]) + + // workaround to make indefinite muting work with Mastodon 3.3.0 + // https://github.com/tuskyapp/Tusky/issues/2107 + val duration = if(binding.duration.selectedItemPosition == 0) { + null + } else { + durationValues[binding.duration.selectedItemPosition] + } + + onOk(binding.checkbox.isChecked, duration) } .setNegativeButton(android.R.string.cancel, null) .show() diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/AttachmentViewData.kt b/app/src/main/java/com/keylesspalace/tusky/viewdata/AttachmentViewData.kt index 52eb31ac..a7b2bffc 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewdata/AttachmentViewData.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/AttachmentViewData.kt @@ -3,7 +3,7 @@ package com.keylesspalace.tusky.viewdata import android.os.Parcelable import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Status -import kotlinx.android.parcel.Parcelize +import kotlinx.parcelize.Parcelize @Parcelize data class AttachmentViewData( diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java b/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java index c0ceeb81..10820fbd 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java @@ -475,6 +475,7 @@ public abstract class StatusViewData { application = viewData.application; statusEmojis = viewData.getStatusEmojis(); accountEmojis = viewData.getAccountEmojis(); + rebloggedByAccountEmojis = viewData.getRebloggedByAccountEmojis(); card = viewData.getCard(); isCollapsible = viewData.isCollapsible(); isCollapsed = viewData.isCollapsed(); diff --git a/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountViewModel.kt index a0f0ed68..1837652e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountViewModel.kt @@ -119,7 +119,7 @@ class AccountViewModel @Inject constructor( } } - fun muteAccount(notifications: Boolean, duration: Int) { + fun muteAccount(notifications: Boolean, duration: Int?) { changeRelationship(RelationShipAction.MUTE, notifications, duration) } diff --git a/app/src/main/res/layout/activity_about.xml b/app/src/main/res/layout/activity_about.xml index 64cbec45..c2d9bdfd 100644 --- a/app/src/main/res/layout/activity_about.xml +++ b/app/src/main/res/layout/activity_about.xml @@ -6,7 +6,9 @@ android:layout_height="match_parent" tools:context="com.keylesspalace.tusky.AboutActivity"> - + - + - + android:visibility="gone" + app:constraint_referenced_ids="accountMovedText,accountMovedAvatar,accountMovedDisplayName,accountMovedUsername" /> - + android:layout_marginTop="12dp" + android:drawablePadding="6dp" + android:textSize="?attr/status_text_medium" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/accountRemoveView" + tools:text="Account has moved" /> + + + + + + + app:layout_constraintTop_toBottomOf="@id/accountMovedAvatar"> + app:layout_constraintTop_toBottomOf="@id/accountMovedAvatar"> + app:layout_constraintTop_toBottomOf="@id/accountMovedAvatar"> - + - + - + - + + tools:context=".LicenseActivity"> - + - + + app:layout_constraintTop_toBottomOf="@id/includedToolbar" /> diff --git a/app/src/main/res/layout/activity_modal_timeline.xml b/app/src/main/res/layout/activity_modal_timeline.xml index 6801d904..05de634d 100644 --- a/app/src/main/res/layout/activity_modal_timeline.xml +++ b/app/src/main/res/layout/activity_modal_timeline.xml @@ -2,12 +2,13 @@ - + - + - + + tools:context=".components.scheduled.ScheduledTootActivity"> - + - + - + diff --git a/app/src/main/res/layout/dialog_mute_account.xml b/app/src/main/res/layout/dialog_mute_account.xml index b58a277c..e826445e 100644 --- a/app/src/main/res/layout/dialog_mute_account.xml +++ b/app/src/main/res/layout/dialog_mute_account.xml @@ -23,7 +23,6 @@ android:text="@string/dialog_mute_hide_notifications"/> diff --git a/app/src/main/res/layout/fragment_timeline.xml b/app/src/main/res/layout/fragment_timeline.xml index 79f7fdb3..d3e716d6 100644 --- a/app/src/main/res/layout/fragment_timeline.xml +++ b/app/src/main/res/layout/fragment_timeline.xml @@ -32,12 +32,12 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:visibility="gone" + app:layout_constrainedHeight="true" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" - tools:visibility="visible" - app:layout_constrainedHeight="true" /> + tools:visibility="visible" /> + \ No newline at end of file diff --git a/app/src/main/res/layout/item_emoji_pref.xml b/app/src/main/res/layout/item_emoji_pref.xml index cb35fe7d..344c102a 100644 --- a/app/src/main/res/layout/item_emoji_pref.xml +++ b/app/src/main/res/layout/item_emoji_pref.xml @@ -2,7 +2,6 @@ + app:layout_constraintStart_toStartOf="@id/emojiName" + app:layout_constraintTop_toBottomOf="@id/emojiName" /> \ No newline at end of file diff --git a/app/src/main/res/layout/item_follow_request.xml b/app/src/main/res/layout/item_follow_request.xml index 36e442c5..e970e128 100644 --- a/app/src/main/res/layout/item_follow_request.xml +++ b/app/src/main/res/layout/item_follow_request.xml @@ -1,52 +1,69 @@ - + android:paddingRight="16dp" + android:paddingBottom="10dp"> + + + android:layout_marginTop="10dp" + android:contentDescription="@string/action_view_profile" + tools:src="@drawable/avatar_default" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/notificationTextView" /> - + android:layout_height="wrap_content" + android:layout_marginStart="14dp" + android:ellipsize="end" + android:maxLines="1" + android:textColor="?android:textColorPrimary" + android:textSize="?attr/status_text_large" + android:textStyle="normal|bold" + app:layout_constraintStart_toEndOf="@id/avatar" + app:layout_constraintTop_toTopOf="@id/avatar" + app:layout_constraintBottom_toTopOf="@id/usernameTextView" + tools:text="Display name" /> - - - - - + - \ No newline at end of file + diff --git a/app/src/main/res/layout/item_follow_request_notification.xml b/app/src/main/res/layout/item_follow_request_notification.xml deleted file mode 100644 index d4db2a3d..00000000 --- a/app/src/main/res/layout/item_follow_request_notification.xml +++ /dev/null @@ -1,96 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/item_follow_requests_header.xml b/app/src/main/res/layout/item_follow_requests_header.xml new file mode 100644 index 00000000..06e5c93f --- /dev/null +++ b/app/src/main/res/layout/item_follow_requests_header.xml @@ -0,0 +1,12 @@ + + diff --git a/app/src/main/res/layout/item_hashtag.xml b/app/src/main/res/layout/item_hashtag.xml index a158240c..efdb24f7 100644 --- a/app/src/main/res/layout/item_hashtag.xml +++ b/app/src/main/res/layout/item_hashtag.xml @@ -1,6 +1,5 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/raw/keep.xml b/app/src/main/res/raw/keep.xml new file mode 100644 index 00000000..c9c47c15 --- /dev/null +++ b/app/src/main/res/raw/keep.xml @@ -0,0 +1,3 @@ + + diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 8348000b..40d9ea8f 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -214,7 +214,9 @@ %1$s, %2$s, %3$s و %4$d آخرون %1$s, %2$s, و %3$s %1$s و %2$s - %d تفاعلات جديدة + + %d تفاعلات جديدة + حساب مقفل عن التطبيق توسكي %s @@ -275,8 +277,10 @@ إزالة الحساب مِن القائمة النشر بواسطة حساب %1$s تعذرت عملية إضافة الشرح - وصف لضعاف البصر -\n(%d أحرف على أقصى تقدير) + + وصف لضعاف البصر +\n(%d أحرف على أقصى تقدير) + إضافة شرح حذف تجميد الحساب @@ -325,7 +329,9 @@ %1$s %1$s و %2$s %1$s و %2$s و %3$d آخَرون - لقد بلغت الحد الأقصى مِن الألسنة %1$d + + لقد بلغت الحد الأقصى مِن الألسنة %1$d + الوسائط: %s تحذير عن المحتوى: %s مِن دون وصف diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 61c97505..0056bfdb 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -76,7 +76,9 @@ Без описание Предупреждение за съдържание: %s Мултимедия: %s - достигнати са максималните %1$d раздела + + достигнати са максималните %1$d раздела + %1$s, %2$s и %3$d други %1$s %1$s и %2$s @@ -134,8 +136,10 @@ Заключване на акаунт Премахване Задаване на надпис - Опишете за хора със зрителни увреждания -\n(%d ограничение на знаците) + + Опишете за хора със зрителни увреждания +\n(%d ограничение на знаците) + Неуспешно задаване на надпис Публикуване с акаунт %1$s Премахване на акаунт от списъка @@ -195,7 +199,9 @@ Tusky %s Относно Заключен акаунт - %d нови взаимодействия + + %d нови взаимодействия + %1$s и %2$s %1$s, %2$s, и %3$s %1$s, %2$s, %3$s и %4$d други @@ -441,7 +447,9 @@ \n Все още можете да осъществите достъп до старите си чернови чрез бутон на екрана за нови чернови, но те ще бъдат премахнати при бъдеща актуализация! Тази публикация не успя да се изпрати! Наистина ли искате да изтриете списъка %s\? - Не можете да качите повече от %1$d мултимедийни прикачени файлове. + + Не можете да качите повече от %1$d мултимедийни прикачени файлове. + Скриване на количествена статистика на профили Скриване на количествена статистика на публикации Ограничаване на известия от емисия diff --git a/app/src/main/res/values-bn-rBD/strings.xml b/app/src/main/res/values-bn-rBD/strings.xml index a2a4ca26..f8aed7f2 100644 --- a/app/src/main/res/values-bn-rBD/strings.xml +++ b/app/src/main/res/values-bn-rBD/strings.xml @@ -12,7 +12,10 @@ বর্ণনা নাই সতর্কবার্তা: %s মিডিয়া: %s - সর্বাধিক %1$d টি ট্যাব পৌঁছেছে + + সর্বোচ্চ %1$dটি ট্যাব পৌঁছেছে + সর্বোচ্চ %1$dটি ট্যাব পৌঁছেছে + দ্বারা পছন্দ দ্বারা সর্মথন পিন @@ -57,8 +60,10 @@ অনুসারী অনুমোদন করার জন্য আপনাকে প্রয়োজন অ্যাকাউন্ট লক করুন ক্যাপশন সেট করুন - দৃষ্টি প্রতিবন্ধী জন্য বর্ণনা করুন -\n(%d অক্ষর সীমা) + + দৃষ্টি প্রতিবন্ধী জন্য বর্ণনা করুন +\n(%d অক্ষর সীমা) + ক্যাপশন সেট করতে ব্যর্থ অ্যাকাউন্ট %1$s থেকে পোস্ট করা হচ্ছে তালিকা থেকে অ্যাকাউন্ট সরান @@ -292,7 +297,7 @@ মিডিয়া লুকানো সংবেদনশীল কন্টেন্ট লাইসেন্সগুলি - খসড়াগুলো + খসড়া আপনার প্রোফাইল সম্পাদনা করুন অনুরোধ অনুসরণ করুন অবরুদ্ধ ব্যবহারকারী @@ -435,4 +440,82 @@ %s তোমার টুট বুস্ট করেছে %s তোমার টুট বুস্ট করেছে ঘোষণা + %1$s,%2$s,%3$s এবং %4$d অন্যরা + যখন আমার সদস্যতা নেওয়া কেউ টুট দেয় তখন বিজ্ঞপ্তি দিবে + নতুন টুট + বিশেষ আবেগ বানাও + সদস্যতা আছে এমন একজন টুট দিয়েছে + কোনো ঘোষণা নেই। + যদিও তোমার অ্যাকাউন্ট রুদ্ধকৃত না, %1$s রা ভেবেছে এই অ্যাকাউন্টগুলোর অনুসরণ অনুরোধ তোমার পরীক্ষা করা উচিত। + নতুন খসড়া বৈশিষ্ট দ্রুততর হওয়ার জন্য নতুনভাবে নকশা করা হয়েছে, যা সহজে ব্যবহারযোগ্য ও কম সমস্যাপূর্ণ। +\n আগের খসড়াগুলো খসড়া পাতার বোতাম দিয়ে যেতে পারো, কিন্তু ভবিষ্যত হালনাগাদে তা সরিয়ে ফেলা হবে! + যে টুটের উত্তর খসড়া করেছিলে তা মুছে ফেলা হয়েছে + এই তালিকাটা আসলেই মুছতে চাও\? + + %1$d টার বেশি সংযুক্তি পাঠানো যাবে না। + %1$d টার বেশি সংযুক্তি পাঠানো যাবে না। + + টুট পাঠাতে ব্যর্থ! + উত্তরের তথ্য আনতে ব্যর্থ + সংরক্ষিত! + অবতারে পরিসংখ্যান লুকাও + ছাপার পরিসংখ্যান লুকাও + সময়কাল বিজ্ঞপ্তি সীমাবদ্ধ করো + তোমার মানসিক স্বাস্থে নেতিবাচক প্রভাব ফেলতে পারে এমন জিনিসগুলো লুকানো আছে। যেমন: +\n +\n - পছন্দ/বুস্ট/অনুসরণ বিজ্ঞপ্তি +\n - টুটে পছন্দ/বুস্ট সংখ্যা +\n - অবতারে অনুসরণকারী/পরিসংখ্যান +\n +\nপুশ-বিজ্ঞপ্তিতে প্রভাব পরবে না, কিন্তু বিজ্ঞপ্তি পছন্দ পাল্টাতে পারবে। + এই অ্যাকাউন্ট নিয়ে তোমার ব্যক্তিগত লেখা + শীর্ষস্থানীয় সরঞ্জামের শিরোনামটি লুকাও + খসড়া মুছো হয়েছে + পুরোনো খসড়া + বিজ্ঞপ্তি + সুস্থতা + সময়হীন + সময়কাল + + %d সেকেন্ড বাকি + %d সেকেন্ড বাকি + + + %d মিনিট বাকি + %d মিনিট বাকি + + + %d ঘন্টা বাকি + %d ঘন্টা বাকি + + + %d দিন বাকি + %d দিন বাকি + + + %s জন + %s জন + + + %sটি ভোট + %sটি ভোট + + %1$s, %2$s এবং %3$d আরো অন্য জন + %1$s এবং %2$s + সদস্যতা + সদস্যতা বাতিল + + %s বুস্ট + %s বুস্ট + + %1$s স্থানান্তরিত হয়েছে এখানে: + সংযুক্তি + শব্দ + + %dটি নতুন ক্রিয়া + %dটি নতুন ক্রিয়া + + %1$s আর %2$s + %1$s, %2$s, আর %3$s + %s তোমাকে উল্লেখ করেছে \ No newline at end of file diff --git a/app/src/main/res/values-bn-rIN/strings.xml b/app/src/main/res/values-bn-rIN/strings.xml index 4fab27c6..35fdd231 100644 --- a/app/src/main/res/values-bn-rIN/strings.xml +++ b/app/src/main/res/values-bn-rIN/strings.xml @@ -225,7 +225,9 @@ %1$s,%2$s,%3$s এবং %4$d আরো অন্য জন %1$s, %2$s, আর %3$s %1$s আর %2$s - %d নতুন মিথস্ক্রিয়া + + %d নতুন মিথস্ক্রিয়া + লক অ্যাকাউন্ট সম্পর্কিত টাস্কি %s @@ -286,8 +288,10 @@ তালিকা থেকে অ্যাকাউন্ট সরান অ্যাকাউন্ট %1$s থেকে পোস্ট করা হচ্ছে ক্যাপশন সেট করতে ব্যর্থ - দৃষ্টি প্রতিবন্ধী জন্য বর্ণনা করুন -\n(%d অক্ষর সীমা) + + দৃষ্টি প্রতিবন্ধী জন্য বর্ণনা করুন +\n(%d অক্ষর সীমা) + ক্যাপশন সেট করুন সরান অ্যাকাউন্ট লক করুন @@ -345,7 +349,9 @@ %1$s %1$s এবং %2$s %1$s, %2$s এবং %3$d আরো অন্য জন - সর্বাধিক %1$d টি ট্যাব পৌঁছেছে + + সর্বাধিক %1$d টি ট্যাব পৌঁছেছে + মিডিয়া: %s সতর্কবার্তা: %s বর্ণনা নাই diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index c0fcd2bd..0cc1d5fe 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -153,7 +153,9 @@ %1$s, %2$s, %3$s i %4$d més %1$s, %2$s i %3$s %1$s i %2$s - %d interaccions noves + + %d interaccions noves + Compte blocat Quant a Tusky %s @@ -286,8 +288,10 @@ Suprimir un compte de la llista Publicar amb el compte %1$s Error al afegir la llegenda - Descriure per a invidentes -\n(%d character limit) + + Descriure per a invidentes +\n(%d character limit) + Afegir una llegenda Eliminar Protegir el compte @@ -342,7 +346,9 @@ %1$s %1$s i %2$s %1$s, %2$s i %3$d més - màxim de %1$d pestanyes aconseguides + + màxim de %1$d pestanyes aconseguides + Mèdia : %s Sense descripció Favorits @@ -493,7 +499,9 @@ Esborranys antics No s\'ha pogut enviar aquest tut! Segur que voleu esborrar la llista %s\? - No podeu pujar més de %1$d adjunts multimèdia. + + No podeu pujar més de %1$d adjunts multimèdia. + Amaga les estadístiques quantitatives dels perfils Amaga les estadístiques quantitatives de les publicacions Limita les notificacions de la cronologia diff --git a/app/src/main/res/values-ckb/strings.xml b/app/src/main/res/values-ckb/strings.xml index 56f4f5e9..5fb07bed 100644 --- a/app/src/main/res/values-ckb/strings.xml +++ b/app/src/main/res/values-ckb/strings.xml @@ -234,7 +234,9 @@ %d کاژێرماوە کاتێک وشەکە یان دەستەواژەکە تەنها ئەبجەدییە، تەنها ئەگەر لەگەڵ هەموو وشەکە یەکبێت کاری پێدەکرێت - ناتوانیت زیاتر لە %1$d هاوپێچی میدیا باربکەیت. + + ناتوانیت زیاتر لە %1$d هاوپێچی میدیا باربکەیت. + شاردنەوەی زانیاری چەندێتی لەسەر پرۆفایلەکان شاردنەوەی زانیاری چەندێتی لە بابەتەکان سنووردارکردنی ئاگانامەکانی تایم لاین @@ -327,7 +329,9 @@ هیچ وەسفێک ئاگاداری ناوەڕۆک: %s میدیا: %s - بەرزترین رێژەی خشتەبەندەکانی %1$d گەیشت + + بەرزترین رێژەی خشتەبەندەکانی %1$d گەیشت + %1$s, %2$s و %3$d زیاتر %1$s و %2$s %1$s @@ -384,8 +388,10 @@ داخستنی ئەژمێر لابردن دانانی سەردێڕ - وەسف بکە بۆ بینایی داڕماو -\n(%d سنوری کاراکتەر) + + وەسف بکە بۆ بینایی داڕماو +\n(%d سنوری کاراکتەر) + دانانی سەردێڕ شکستی هێنا بڵاوکردنەوە بە هەژماری %1$s لابردنی ئەژمێر لە لیستەکە @@ -442,7 +448,9 @@ توسکی %s سەبارەت هەژماری داخراو - %d چالاکی نوێ + + %d چالاکی نوێ + %1$s و %2$s %1$s و %2$s و %3$s %1$s, %2$s, %3$s و %4$d ئەوانی تر diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 68c7188e..dd90d890 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -220,7 +220,9 @@ %1$s, %2$s, %3$s a dalších %4$d %1$s, %2$s a %3$s %1$s a %2$s - %d nových interakcí + + %d nových interakcí + Uzamčený účet O této aplikaci Tusky %s @@ -285,7 +287,9 @@ Odstranit účet ze seznamu Píšete s účtem %1$s Nastavení popisku selhalo - Popis pro zrakově postižené\n(limit %d znaků) + + Popis pro zrakově postižené\n(limit %d znaků) + Nastavit popisek Odstranit Uzamknout účet @@ -344,7 +348,9 @@ %1$s %1$s a %2$s %1$s, %2$s a %3$d další - bylo dosaženo maxima %1$d panelů + + bylo dosaženo maxima %1$d panelů + Média %s Varování o obsahu: %s @@ -475,7 +481,8 @@ Odkrýt oznámení od %s Odkrýt %s Ztišit @%s\? - %s požádal/a aby vás mohl/a sledovat Zobrazit dialogové okno s potvrzením při boostování + %s právě vydal + Oznámení \ No newline at end of file diff --git a/app/src/main/res/values-cy/strings.xml b/app/src/main/res/values-cy/strings.xml index ad210b07..2c30322e 100644 --- a/app/src/main/res/values-cy/strings.xml +++ b/app/src/main/res/values-cy/strings.xml @@ -190,7 +190,9 @@ %1$s, %2$s, %3$s a %4$d eraill %1$s, %2$s, a %3$s %1$s a %2$s - %d rhyngweithiad newydd + + %d rhyngweithiad newydd + Cyfrif wedi\'i gloi Amdano Mae Tusky yn feddalwedd ffynhonnell agored barn rydd. diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 4ea468a9..c2ea3ae1 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -217,7 +217,10 @@ %1$s, %2$s, %3$s und %4$d andere %1$s, %2$s, und %3$s %1$s und %2$s - %d neue Interaktionen + + %d neue Interaktion + %d neue Interaktionen + Gesperrtes Profil Über Tusky ist freie und quelloffene Software. Es ist lizenziert unter der GNU General Public License Version 3. Du kannst dir die Lizenz hier anschauen: https://www.gnu.org/licenses/gpl-3.0.de.html @@ -261,7 +264,9 @@ Ein Konto zu einer Liste hinzufügen verfassen mit %1$s Fehler beim Speichern der Beschreibung - Für Menschen mit Sehbehinderung beschreiben\n(%d Zeichen) + + Für Menschen mit Sehbehinderung beschreiben\n(%d Zeichen) + Beschreibung eingeben Entfernen Gesperrtes Profil @@ -310,13 +315,16 @@ %1$s %1$s und %2$s %1$s, %2$s und %3$d mehr - Maximum von %1$d Tabs erreicht + + Maximum von %1$d Tab erreicht + Maximum von %1$d Tabs erreicht + Keine Beschreibung Favorisiert Öffentlich Folgende Direkt - Name auflisten + Listenname Medien herunterladen Medien werden heruntergeladen zu filternde Phrase @@ -477,7 +485,10 @@ \nDu kannst deine alten Entwürfe noch hinter einem Button bei den neuen Entwürfen finden, aber sie werden mit einem zukünftigen Update gelöscht! Dieser Beitrag konnte nicht gesendet werden! Willst du die Liste %s wirklich löschen\? - Du kannst nicht mehr als %1$d Anhänge hochladen. + + Du kannst nicht mehr als %1$d Anhang hochladen. + Du kannst nicht mehr als %1$d Anhänge hochladen. + Wohlbefinden Dauer Für immer @@ -488,4 +499,19 @@ GIF-Emojis animieren Jemand, den ich abonniert habe, etwas Neues veröffentlicht %s hat gerade etwas gepostet + %dm + Benachrichtigungen überprüfen + Informationen, die dein Wohlbefinden beeinflussen könnten, werden versteckt. Das beinhaltet +\n +\n- Benachrichtigungen über favorisierte/geteilte Beiträge, sowie \"Jemand folgt dir\" Benachrichtigungen +\n- Anzahl der Favoriten/Teilungen von Beiträgen +\n- Statistiken zu Followern auf Profilen +\n +\nPush-Benachrichtigungen sind nicht betroffen, aber du kannst diese manuell überprüfen. + Auch wenn dein Konto nicht gesperrt ist, haben die Admins von %1$s gedacht, dass es besser wäre diese Folgenden manuell zu bestätigen. + Keine Statistiken auf Profilen zeigen + Keine Statistiken in Posts zeigen + Timeline-Benachrichtigungen einschränken + Abonnieren + nicht mehr abonnieren \ No newline at end of file diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml new file mode 100644 index 00000000..4734fea9 --- /dev/null +++ b/app/src/main/res/values-el/strings.xml @@ -0,0 +1,6 @@ + + + Αυτό δεν μπορεί να είναι κενό. + Προέκυψε σφάλμα δικτύου! Παρακαλώ ελέγξτε τη σύνδεσή σας και προσπαθήστε ξανά! + Προέκυψε ένα σφάλμα. + \ No newline at end of file diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index bfe88277..2529448b 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -216,7 +216,9 @@ %1$s, %2$s, %3$s kaj %4$d aliaj %1$s, %2$s, kaj %3$s %1$s kaj %2$s - %d novaj interagoj + + %d novaj interagoj + Ŝlosita konto Pri Tusky %s @@ -281,7 +283,9 @@ Forigi konton el la listo Afiŝi per konto %1$s Redakto de apudskribo malsukcesis - Priskribi por misvidantaj homoj\n(%d signoj maksimume) + + Priskribi por misvidantaj homoj\n(%d signoj maksimume) + Redakti apudskribon Forigi Ŝlosi konton @@ -338,7 +342,9 @@ %1$s %1$s kaj %2$s %1$s, %2$s kaj %3$d aliaj - maksimuma nombro %1$d da langetoj atingita + + maksimuma nombro %1$d da langetoj atingita + Aŭdovidaĵo: %s Enhava averto: %s diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 93e90774..93127439 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -203,7 +203,9 @@ %1$s, %2$s, %3$s y %4$d otros %1$s, %2$s, y %3$s %1$s y %2$s - %d nuevas interacciones + + %d nuevas interacciones + Cuenta protegida Acerca de Tusky %s @@ -251,7 +253,9 @@ Cronología de lista Publicando con la cuenta %1$s Error al añadir leyenda - Describir para invidentes\n(límite de %d caracteres) + + Describir para invidentes\n(límite de %d caracteres) + Añadir leyenda Eliminar Proteger cuenta @@ -308,7 +312,9 @@ %1$s %1$s y %2$s %1$s, %2$s y %3$d más - máximo de %1$d pestañas alcanzadas + + máximo de %1$d pestañas alcanzadas + Menciones Mostrar favoritos Menciones @@ -480,7 +486,9 @@ No hay anuncios. Anuncios %s recién publicado - No puedes cargar más de %1$d archivos adjuntos multimedia. + + No puedes cargar más de %1$d archivos adjuntos multimedia. + Esconder las estadísticas cuantitativas de los perfiles Esconder las estadísticas cuantitativas de las publicaciones Revisar Notificaciones diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index 8498bba0..7362a720 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -34,7 +34,7 @@ Profila editatu Zirriborroak Lizentziak - %s-(e)k bultzatu du + %s(e)k bultzatu du Kontuz edukiarekin Ezkutuko multimedia Sakatu ikusteko @@ -43,8 +43,8 @@ Zabaldu Bildu Edukirik ez. Arrastatu behera birkargatzeko! - %s-(e)k zure tuta bultzatu du - %s-(e)k zure tuta gogoko du + %s(e)k zure tuta bultzatu du + %s(e)k zure tuta gogoko du %s(e)k jarraitu zaitu \@%s salatu Informazio gehigarria? @@ -188,11 +188,13 @@ Bultzatutako tuten jakinarazpenak Gogokoak Zure tutak gogoko bezala ezartzerakoan jakinarazpenak - %s-(e)k aipatu zaitu + %s(e)k aipatu zaitu %1$s, %2$s, %3$s eta beste %4$d %1$s, %2$s eta %3$s %1$s eta %2$s - %d interakzio berri + + %d interakzio berri + Kontu babestua Honi buruz Tusky software libre eta kode askekoa da. @@ -226,7 +228,7 @@ Jarraitzen zaitu Eduki mingarria erakutsi Multimedia - \@%s-ri erantzuten + \@%s-(r)i erantzuten Gehiago erakutsi Gehitu kontua Mastodon kontua gehitu @@ -235,7 +237,9 @@ Zerrenda denbora-lerroa %1$s kontuarekin tut egiten Akatsa deskribapena eranstean - Ikusmen urritasuna dutenentzat deskribapena\n(%d karaktereko muga) + + Ikusmen urritasuna dutenentzat deskribapena\n(%d karaktereko muga) + Deskribapena erantsi Ezabatu Kontua babestu @@ -309,7 +313,7 @@ Media jaisten %s ez dago ezkutatua Tut hau ezabatu eta zirriborro berria egin\? - Ziur al zaude %s ezabatu nahi duzula\? Domeinu horretatik datorren edukia ez duzu denbora-lerro publikoetan edo jakinarazpenentan ikusiko. Domeinu horretan dituzun jarraitzaileak ezabatuko dira. + Ziur al zaude %s ezabatu nahi duzula\? Domeinu horretatik datorren edukia ez duzu denbora-lerro publikoetan edo jakinarazpenetan ikusiko. Domeinu horretan dituzun jarraitzaileak ezabatuko dira. Domeinu osoa ezkutatu Galdeketak bukatu dira Iragazkiak @@ -361,7 +365,9 @@ %1$s %1$s eta %2$s %1$s, %2$s eta %3$d gehiago - gehienezko %1$d fitxa iritsita + + gehienezko %1$d fitxa iritsita + Media: %s Edukiaren abisua: %s Deskribapenik ez @@ -377,7 +383,7 @@ Garbitu Iragazi Aplikatu - Idatzi Toot-a + Idatzi tuta Idatzi Ziur zaude jakinarazpen guztiak betirako garbitu nahi dituzula\? %s irudiarentzako ekintzak @@ -450,7 +456,6 @@ Traolak Ez erakutsi jakinarazpenak Desmututu %s - Ezkutatu goiko tresna-barraren izenburua Erakutsi berrespen-abisua tuta bultzatu aurretik Erakutsi esteken aurrebista denbora-lerroetan @@ -464,7 +469,7 @@ Goia Nabigatze posizio nagusia Erakutsi gradiente koloretsua ezkutuko mediarentzako - jarraipena-eskaera + jarraipen-eskaera Desmututu elkarrizketa Desmututu %s Mututu %s(r)en jakinarazpenak diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 68fa3bc5..50f987e2 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -192,7 +192,10 @@ %1$s، %2$s، %3$s و %4$d دیگر %1$s، %2$s و %3$s %1$s و %2$s - %d برهم‌کنش جدید + + %d برهم‌کنش جدید + %d برهم‌کنش جدید + حساب قفل‌شده درباره تاسکی نرم‌افزاری آزاد است که تحت نگارش ۳ از پروانهٔ جامع همگانی گنو منتشر شده است. پروانه را می‌توانید از این‌جا ببینید: https://www.gnu.org/licenses/gpl-3.0.en.html @@ -230,8 +233,10 @@ خط زمانی فهرست در حال فرستادن با حساب %1$s شکست در تنظیم عنوان - توصیف برای کم‌بینایان -\n(کران %d نویسه) + + توصیف برای کم‌بینایان +\n(کران %d نویسه) + تنظیم عنوان برداشتن قفل حساب @@ -352,7 +357,10 @@ %1$s %1$s و %2$s %1$s، %2$s و %3$d بیش‌تر - رسیده به بیشینهٔ %1$d زبانه + + رسیده به بیشینهٔ %1$d زبانه + رسیده به بیشینهٔ %1$d زبانه + رسانه: %s هشدار محتوا: %s بدون هیچ توضیحی @@ -474,4 +482,40 @@ یادداشت خصوصیتان دربارهٔ این حساب هیچ اعلامیه‌ای وجود ندارد. اعلامیه‌ها + عدم اشتراک + اشتراک + پیش‌نویس حذف شد + پیش‌نویس‌های قدیمی + فرستادن این بوق شکست خورد! + نهفتن آمار کمی روی نمایه‌ها + نهفتن آمار کمی روی فرسته‌ها + محدود کردن آگاهی‌های خط‌زمانی + بازبینی آگاهی‌ها + سلامتی + طول + پیوست‌ها + صدا + آگاهی‌ها هنگام انتشار بوقی جدید از کسی که مشترکش هستید + بوق‌های جدید + اموجی‌های شخصی متحرّک + کسی که مشترکش شده‌ام، بوقی جدید منتشر کرد + %s چیزی فرستاد + بوقی که پاسخی به آن را پیش‌نویس کردید، برداشته شده + شکست در بار کردن اطّلاعات پاسخ + برخی اطّلاعات که ممکن است روی سلامتی ذهنیتان تأثیر بگذارد، پنهان خواهند شد. همچون: +\n +\n - آگاهی‌های برگزیدن، تقویت و پی‌گیری +\n - شمار برگزیدن و تقویت بوق‌ها +\n - آمار پی‌گیر و فرسته روی نمایه‌ها +\n +\n فرستادن آگاهی‌ها تأثیر نمی‌پذیرد، ولی می‌توانید ترجیحات آگاهیتان را به صورت دستی بازبینی کنید. + ویژگی پیش‌نویس در تاسکی به صورت کامل بازطرّاحی شده تا سریع‌تر، کاربرپسندتر و کم‌مشکل‌تر باشد. +\n همجنان می‌توانید از طریق دکمه‌ای دز صفحهٔ پیش‌نویس‌های جدید، به پیش‌نویس‌های قدیمیتان دسترسی داشته باشید، ولی در به‌روز رسانی آینده برداشته خواهند شد! + واقعاً می‌خواهید فهرست %s را حذف کنید؟ + + نمی‌توانید بیش از %1$d رسانه بارگذارید. + نمی‌توانید بیش از %1$d رسانه بارگذارید. + + نامعیّن + با این که حسابتان قفل نیست، کارکنان %1$s فکر کردند ممکن است بخواهید درخواست‌های پی‌گیری از این حساب‌ها را دستی بازبینی کنید. \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index a677b67e..729fdc1e 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -220,7 +220,9 @@ %1$s, %2$s, %3$s et %4$d autres %1$s, %2$s et %3$s %1$s et %2$s - %d nouvelles interactions + + %d nouvelles interactions + Compte verrouillé À propos Tusky %s @@ -285,8 +287,10 @@ Supprimer un compte de la liste Publier avec le compte %1$s Impossible de définir la légende - Décrire pour les malvoyants -\n(%d caractères maximum) + + Décrire pour les malvoyants +\n(%d caractères maximum) + Mettre une légende Supprimer le média Verrouiller le compte @@ -343,7 +347,9 @@ %1$s %1$s et %2$s %1$s, %2$s et %3$d autres - nombre maximum d\'onglets %1$d atteint + + nombre maximum d\'onglets %1$d atteint + Média : %s Avertissement : %s @@ -499,7 +505,9 @@ %s vient de publier Examiner les notifications Nouveau pouets - Vous ne pouvez pas téléverser plus de %1$d pièces jointes. + + Vous ne pouvez pas téléverser plus de %1$d pièces jointes. + Bien-être Notifications quand quelqu\'un que vous suivez publie un nouveau pouet Limiter les notifications de la timeline diff --git a/app/src/main/res/values-ga/strings.xml b/app/src/main/res/values-ga/strings.xml index 2ba114b5..6e7cba17 100644 --- a/app/src/main/res/values-ga/strings.xml +++ b/app/src/main/res/values-ga/strings.xml @@ -190,7 +190,9 @@ Cumhachtaithe ag Tusky Tusky %s Cuntas faoi Ghlas - %d idirghníomhaíochtaí nua + + %d idirghníomhaíochtaí nua + %1$s agus %2$s %1$s, %2$s, agus %3$s %1$s, %2$s, %3$s agus %4$d cinn eile @@ -290,7 +292,9 @@ Sraith emoji reatha Google Tá cód agus sócmhainní ó na tionscadail foinse oscailte seo a leanas i Tusky: Féadfaidh an fhaisnéis thíos próifíl an úsáideora a léiriú go neamhiomlán. Brúigh chun próifíl iomlán a oscailt sa bhrabhsálaí. - uasmhéid de chluaisíní %1$d sroichte + + uasmhéid de chluaisíní %1$d sroichte + Vótaíocht le roghanna: %1$s, %2$s, %3$s, %4$s; %5$s Liosta Cumadh Tút @@ -319,8 +323,10 @@ Bain cuntas ón liosta Postáil le cuntas %1$s Theip ar an bhfotheideal a shocrú - Déan cur síos ar dhaoine lagamhairc -\n(teorainn carachtar %d) + + Déan cur síos ar dhaoine lagamhairc +\n(teorainn carachtar %d) + Socraigh fotheideal Bain Cuntas glasála diff --git a/app/src/main/res/values-gd/strings.xml b/app/src/main/res/values-gd/strings.xml index 83bfce33..13d53a29 100644 --- a/app/src/main/res/values-gd/strings.xml +++ b/app/src/main/res/values-gd/strings.xml @@ -2,54 +2,535 @@ Liostaichean Liostaichean - Mu dheidhinn + Mu dhèidhinn Ath-shuidhich Lorg - Roighainnean cunntais - Roighainnean + Roghainnean a’ chunntais + Roghainnean Clàraich a-mach - Dreachd - Prìomhaich - Dè a th ’ann an àite\? + Dreachdan + Annsachdan + Dè a th’ ann an ionstans\? Deasaich - Deasaich sìoltachan + Deasaich a’ chriathrag Deasaich an liosta - Fiosan + Brathan Deasaich Deasaich - Deasaich do phròifil - Deasaich pròifil - Ainm scrion + Deasaich a’ phròifil agad + Deasaich a’ phròifil + Ainm-taisbeanaidh Rabhadh susbaint Dè tha dol\? Sgeul-beatha - Freagairt… + Freagair… Lorg… Clàraich a-steach le Mastodon - TÚT! - Meur-chlàr emoji - Sguir a leantainn + POSTAICH! + Meur-chlàr Emoji + Na lean tuilleadh Lean - Tuilleadh - Ath-ghairm + Barrachd + Feuch ris a-rithist Dùin - TÙT - Cuir às - Cuir às agus ath-sgrìobhadh - Thoir cunntas air + POSTAICH + Sguab às + Sguab às is dèan dreachd ùr air + Dèan gearan Dì-bhac - Cuir am falach boosts - Seall boosts - Caisg - Dèan-sgrìobhaiche + Falaich na brosnachaidhean + Seall na brosnachaidhean + Bac + Sgrìobh Rabhadh susbaint - Dèan-sgrìobhaiche - Dèan-sgrìobhaiche - Seall boosts - Seall boosts + Sgrìobh + Sgrìobh + Seall na brosnachaidhean + Seall na brosnachaidhean Dreachdan - Prìomhaich - Fiosan - Fiosan - Prìomhaich + Annsachdan + Brathan + Brathan + Annsachdan + Cuir crìoch air an fho-sgrìobhadh + Fo-sgrìobh + Beòthaich na h-Emojis gnàthaichte + Bha againn ris a’ phost a bha thu airson freagairt dha a thoirt air falbh + Chaidh an dreach a sguabadh às + Cha deach leinn fiosrachadh na freagairte a luchdadh + Seann-dreachdan + Chaidh dealbhadh gu tur ùr a chur air gleus nan dreachdan aig Tusky ach am biodh e nas luaithe, nas fhasa cleachdadh is nas lugha de bhugaichean ann. +\n Gheibh thu grèim air na seann-dreachdan agad fhathast le putan air sgrìn ùr nan dreachdan ach thèid an toirt air falbh le ùrachadh ri teachd! + Cha b’ urrainn dhuinn am post a chur! + Ceanglachain + Fuaim + A bheil thu cinnteach gu bheil thu airson an liosta %s a sguabadh às\? + 7 làithean + 3 làithean + Latha + 6 uairean a thìde + Uair a thìde + Leth-uair a thìde + 5 mionaidean + Gun chrìoch + Faide + + Chan urrainn dhut barrachd air %1$d cheanglachan meadhain a luchdadh suas. + Chan urrainn dhut barrachd air %1$d cheanglachan meadhain a luchdadh suas. + Chan urrainn dhut barrachd air %1$d ceanglachain meadhain a luchdadh suas. + Chan urrainn dhut barrachd air %1$d ceanglachan meadhain a luchdadh suas. + + Falaich an stadastaireachd àireamhail air pròifilean + Falaich an stadastaireachd àireamhail air postaichean + Cuingich na brathan mun loidhne-ama + Thoir sùil air na brathan + Thèid cuid a dh’fhiosrachadh a dh’fhaodadh droch-bhuaidh a thoirt air d’ shlàinte-inntinn fhalach. Tha seo a’ gabhail a-staigh: +\n +\n - Brathan air annsachdan/brosnachaidhean/leantainn +\n - Cunntas nan annsachdan/brosnachaidhean air postaichean +\n - Stadastaireachd an luchd-leantainn/nam postaichean air pròifilean +\n +\n Cha doir seo buaidh air na brathan-putaidh ach ’s urrainn dhut roghainnean nam brathan agad atharrachadh a làimh. + Slàinte-inntinn + Brathan nuair a dh’fhoillsich cuideigin air a rinn mi fo-sgrìobhadh post ùr + Postaichean ùra + dh’fhoillsich cuideigin air a rinn mi fo-sgrìobhadh post ùr + Tha %s air rud a phostadh + Chan eil brath-fios ann. + Brathan-fios + Chaidh a shàbhaladh! + Nòta prìobhaideach agad mun chunntas seo + Falaich tiotal a’ bhàir-inneal aig a’ bhàrr + Falaich na brathan + Mùch na brathan o %s + Dì-mhùch na brathan o %s + Seall còmhradh dearbhaidh mus dèan thu brosnachadh + Seall ro-sheallaidhean air ceanglaichean sna loidhnichean-ama + Feumaidh co-dhiù 5 mionaidean a bhith eadar staidean sgeidealaichte air Mastodon. + Chan eil staid sam bith air an sgeideal agad. + Chan eil dreachd sam bith agad. + Thachair mearachd le lorg a’ phuist %s + Roghainn %d + Iomadh roghainn + Cuir roghainn ris + Cunntas-bheachd + Cuir an comas gluasad grad-shlaighdidh airson leum a ghearradh o thaba gu taba + Seall criathrag nam brathan + Dh’fhàillig leis an lorg + Cunntasan + Chaidh an cunntas a chlàradh air frithealaiche eile. A bheil thu airson lethbhreac dhen ghearan a chur dha-san gun ainm cuideachd\? + Thèid do ghearan a chur gu maor an fhrithealaiche agad. ’S urrainn dhut mìneachadh a sholar air carson a tha thu a’ gearan mun chunntas gu h-ìosal: + Cha b’ urrainn dhuinn na staidean fhaighinn + Cha b’ urrainn dhuinn do ghearan a chlàradh + Sìn air adhart gu %s + Beachdan a bharrachd + Chaidh do gearan air @%s a chlàradh + Deiseil + Air ais + Air adhart + + Tha %d diog air fhàgail + Tha %d dhiog air fhàgail + Tha %d diogan air fhàgail + Tha %d diog air fhàgail + + + Tha %d mhionaid air fhàgail + Tha %d mhionaid air fhàgail + Tha %d mionaidean air fhàgail + Tha %d mionaid air fhàgail + + + Tha %d uair a thìde air fhàgail + Tha %d uair a thìde air fhàgail + Tha %d uairean a thìde air fhàgail + Tha %d uair a thìde air fhàgail + + + Tha %d latha air fhàgail + Tha %d latha air fhàgail + Tha %d làithean air fhàgail + Tha %d latha air fhàgail + + Thàinig cunntas-bheachd sa chruthaich thu gu crìoch + Thàinig cunntas-bheachd sa bhòt thu gu crìoch + Bhòt + air a dhùnadh + thig e gu crìoch %s + + %s duine + %s dhuine + %s dhaoine + %s duine + + + %s bhòt + %s bhòt + %s bhòtaichean + %s bhòt + + %1$s • %2$s + Gnìomhan dhan dealbh %s + A bheil thu cinnteach gu bheil thu airson na brathan uile agad fhalamhachadh gu buan\? + Sgrìobh post + Cuir an sàs + Criathraich + Falamhaich + Liosta + Tagh liosta + Taga hais gun # + Cuir taga hais ris + Ainm na liosta + Cunntas-bheachd le roghainnean: %1$s, %2$s, %3$s, %4$s; %5$s + Dìreach + ’Na chomharra-lìn + ’Na annsachd + Air ath-bhlogadh + Gun tuairisgeul + Rabhadh susbainte: %s + Meadhan: %s + + ràinig thu na tha ceadaichte dhe %1$d taba + ràinig thu na tha ceadaichte dhe %1$d thaba + ràinig thu na tha ceadaichte dhe %1$d tabaichean + ràinig thu na tha ceadaichte dhe %1$d taba + + %1$s, %2$s ’s %3$d eile + %1$s + ’Na annsachd aig + ’Ga brosnachadh le + + %s bhrosnachadh + %s bhrosnachadh + %s brosnachaidhean + %s brosnachadh + + + %1$s annsachd + %1$s annsachd + %1$s annsachdan + %1$s annsachd + + Prìnich + Dì-phrìnich + Dh’fhaoidte nach fhaic thu pròifil gu lèir a’ chleachdaiche gu h-ìosal. Dèan brùthadh gus a’ phròifil shlàn fhosgladh ann am brabhsair. + Cleachd àm absaloideach + Susbaint + Leubail + cuir dàta ris + Meata-dàta na pròifile + CC-BY-SA 4.0 + CC-BY 4.0 + Fo cheadachas Apache License (chì thu lethbhreac dheth gu h-ìosal) + Tha còs is maoin o na pròiseactan open source seo am broinn Tusky: + Na brosnaich tuilleadh + Brosnaich dhan èisteachd tùsail + Chaidh %1$s a ghluasad gu: + Robotair + Dh’fhàillig an luchdadh a-nuas + Seata làithreach nan Emoji aig Google + Seata stannardach nan Emoji aig Mastodon + Emojis Blob aig Android 4.4–7.1 + Seata tùsail nan Emojis air an uidheam agad + Ath-thòisich + Uaireigin eile + Feumaidh tu Tusky ath-thòiseachadh gus na roghainnean seo a chur an sàs + Feumaidh tu an aplacaid ath-thòiseachadh + Fosgail am post + Leudaich/Co-theannaich gach staid + ’Ga lorg… + Feumaidh tu na seataichean seo de dh’Emojis a luchdadh a-nuas an toiseach + Bun-roghainn an t-siostaim + Stoidhle nan Emojis + Chaidh lethbhreac dheth a chur air an stòr-bhòrd + Chan eil Emojis gnàthaichte aig an ionstans %s agad + Chaidh lethbhreac dhen phost agad a shàbhaladh ’na dhreachd + Chaidh sgur dhen chur + A’ cur nam post + Mearachd a’ cur a’ phuist + A’ cur a’ phuist… + A bheil thu airson a shàbhaladh ’na dhreachd\? + Feumaidh tu gabhail ri luchd-leantainn ùr a làimh + Glais an cunntas + Suidhidh am fo-thiotal + + Mìnich e dhan fheadhainn air a bheil cion-lèirsinn +\n(%d caractar(an) air a char as fhaide) + + Cha deach leinn am fo-thiotal a shuidheachadh + A’ postadh leis a’ chunntas %1$s + Thoir an cunntas air falbh on liosta + Cuir cunntas ris an liosta + Lorg daoine air a leanas tu + Sguab às an liosta + Thoir ainm ùr air an liosta + Cruthaich liosta + Cha b’ urrainn dhuinn an liosta a sguabadh às + Cha b’ urrainn dhut ainm ùr a thoirt air an liosta + Cha b’ urrainn dhuinn an liosta a chruthachadh + Loidhne-ama na liosta + Cuir cunntas Mastodon ùr ris + Cuir cunntas ris + An abairt ri chriathradh + Mur eil ach litrichean is àireamhan san fhacal-luirg, cha dèid a chur an sàs ach ma bhios e a’ maidseadh an fhacail shlàin + Leudaich postaichean ris a bheil rabhadh susbainte an-còmhnaidh + Co-roinn ceangal dhan phost + Co-roinn susbaint a’ phuist + ’S e bathar-bog saor le bun-tùs fosgailte a th’ ann an Tusky. Tha e fo cheadachas GNU General Public License tionndadh 3. Chì thu an ceadachas an-seo: https://www.gnu.org/licenses/gpl-3.0.en.html + Brathan nuair a thèid post agad a chomharrachadh ’na annsachd + Brathan nuair a thèid post agad brosnachadh + A bheil thu airson am post seo a sguabadh às is dreachd ùr a dhèanamh air\? + A bheil thu airson am post seo a sguabadh às\? + ’S urrainn dhut seòladh no àrainn-lìn aig ionstans sam bith a chur a-steach an-seo, can mastodon.social, icosahedron.website, social.tchncs.de agus a bharrachd! +\n +\nMur eil cunntas agad fhathast, cuir a-steach ainm an ionstans sa bheil thu airson ballrachd fhaighinn airson cunntas a chruthachadh ann. +\n +\n’S e an t-aon àite far an cruthaich thu cunntas a th’ ann an ionstans ud ’s a nì an t-òstadh dhan chunntas agad. Gidheadh, ’s urrainn dhut conaltradh le daoine a tha air ionstans eile agus leantainn orra mar gun robh sibh air an aon làrach. +\n +\nGheibh thu barrachd fiosrachaidh air joinmastodon.org. + Co-roinn am post le… + Co-roinn URL a’ phuist le… + Cuir post air an sgeideal + Faicsinneachd a’ phuist + Postaichean air an sgeideal + Chuir %s am post agad ris na h-annsachdan + Bhrosnaich %s am post agad + Postaichean air an sgeideal + Post + Mearachd a’ cur a’ phuist. + Dì-mhùch %s + Tagaichean hais + Luchd-leantainn + Neo-liostaichte + Poblach + %1$s ’s %2$s + Thoir air falbh + Facal slàn + Ùraich + Thoir air falbh + Cuir criathrag ris + Còmhraidhean + Loidhnichean-ama poblach + luchdaich barrachd dheth + A’ freagairt gu @%s + Meadhanan + Seall susbaint fhrionasach an-còmhnaidh + ’Gad leantainn + %dd + %dm + %du + %dl + %db + an ceann %dd + an ceann %dm + an ceann %du + an ceann %dl + an ceann %db + Iarrar leantainn orm + Videothan + Dealbhan + Pròifil Tusky + Aithrisean air bugaichean ⁊ iarrtasan air gleusan: +\n https://github.com/tuskyapp/Tusky/issues + Làrach-lìn a’ phròiseict: +\n https://tusky.app + Le cumhachd Tusky + Tusky %s + Cunntas glaiste + + %d eadar-ghabhail ùr + %d eadar-ghabhail ùr + %d eadar-ghabhailean ùra + %d eadar-ghabhail ùr + + %1$s ’s %2$s + %1$s, %2$s ’s %3$s + %1$s, %2$s, %3$s ’s %4$d eile + Thug %s iomradh ort + Brathan mu chunntasan-bheachd a thàinig gu crìoch + Cunntasan-bheachd + Brosnachaidhean + Brathan mu iarrtasan leantainn + Iarrtasan leantainn + Brathan mu luchd-leantainn ùr + Luchd-leantainn ùr + Brathan mu iomraidhean ùra + Iomraidhean ùra + As motha + Mòr + Meadhanach + Beag + As lugha + Meud teacsa na staid + Luchd-leantainn a-mhàin + Neo-liostaichte + Poblach + Aig a’ bhonn + Aig a’ bhàrr + Prìomh-ionad na seòladaireachd + Dh’fhàillig le sioncronachadh nan roghainnean + ’Ga fhoillseachadh (ga shioncronachadh le frithealaiche) + Cuir comharra ri meadhanan an-còmhnaidh gu bheil iad frionasach + Prìobhaideachd thùsail nam post + Port progsaidh HTTP + Frithealaiche progsaidh HTTP + Cuir an comas a’ phrogsaidh HTTP + Progsaidh HTTP + Progsaidh + Luchdaich a-nuas ro-sheallaidhean air meadhanan + Seall na freagairtean + Tabaichean + Criathradh na loidhne-ama + Seall caiseadan dathte an àite meadhanan falaichte + Beothaich avataran GIF + Seall taisbeanair do bhotaichean + Cànan + Falaich am putan sgrìobhaidh fhad ’s a bhios mi ri sgroladh + Cleachd tabaichean Chrome gnàthaichte + Brabhsair + Cleachd co-dhealbhachd an t-siostaim + Gu fèin-obrachail aig beul na h-oidhche + Dubh + Soilleir + Dorcha + Criathragan + Loidhnichean-ama + Ùrlar na h-aplacaid + Coltas + thig cunntas-bheachd gu crìoch + thèid post agam a chur ris na h-annsachdan + thèid post agam a bhrosnachadh + iarrar leantainn orm + leanar orm + thoirear iomradh orm + Cuir brath thugam nuair a + Seall solas nuair a thig brath a-steach + Dèan crith nuair a thig brath a-steach + Seirm nuair a thig brath a-steach + Rabhaidhean + Dìreach: Postaich dha na cleachdaichean le iomradh orra a-mhàin + Luchd-leantainn a-mhàin: Postaich dhan luchd-leantainn a-mhàin + Neo-liostaichte: Na seall air loidhnichean-ama poblach + Poblach: Postaich gu loidhnichean-ama poblach + A bheil thu airson @%s a mhùchadh\? + A bheil thu airson @%s a bhacadh\? + Falaich an àrainn uile gu lèir + A bheil thu cinnteach gu bheil thu airson %s a bhacadh uile gu lèir\? Chan fhaic thu susbaint on àrainn ud air loidhne-ama phoblach sam bith no am measg nam brathan agad. Thèid an luchd-leantainn agad on àrainn ud a thoirt air falbh. + A bheil thu airson sgur de leantainn air a’ chunntas seo\? + A bheil thu airson an t-iarrtas leantainn a chùl-ghairm\? + Luchdaich a-nuas + ’Ga luchdadh suas… + A’ crìochnachadh luchdadh suas meadhanan + A’ dèanamh ceangal… + Bann-cinn + Avatar + Chan eil toradh ann + Cò an t-ionstans\? + Chaidh an fhreagairt a chur. + Chaidh a chur! + Chan eil %s falaichte tuilleadh + Chaidh an cleachdaiche dhì-mhùchadh + Chaidh an cleachdaiche a dhì-bhacadh + Chaidh a chur! + Co-roinn am meadhan le… + A’ luchdadh a-nuas meadhanan + Luchdaich a-nuas meadhanan + Co-roinn mar … + Fosgail mar %s + Dèan lethbhreac dhen cheangal + A’ luchdadh a-nuas %1$s + Fosgail meadhan #%d + Ceanglaichean + Iomraidhean + Tagaichean hais + Seall na h-annsachdan + Fosgail ùghdar a’ bhrosnachaidh + Tagaichean hais + Iomraidhean + Ceanglaichean + Cuir taba ris + Diùlt + Gabh ris + Neo-dhèan + Sàbhail + Fosgail an drathair + Falaich na meadhanan + Iomradh + Dì-mhùch an còmhradh + Mùch an còmhradh + Dì-mhùch %s + Mùch %s + Dì-mhùch + Mùch + Co-roinn + Tog dealbh + Cuir cunntas-bheachd ris + Cuir meadhan ris + Fosgail sa bhrabhsair + Meadhanan + Iarrtasan leantainn + Àrainnean falaichte + Cleachdaichean bacte + Cleachdaichean mùchte + Comharran-lìn + Pròifil + A bheil thu cinnteach gu bheil thu airson clàradh a-mach às a’ chunntas %1$s\? + Thoir air falbh o na h-annsachdan + Cuir ris na comharran-lìn + Cuir ris na h-annsachdan + Thoir am brosnachadh air falbh + Brosnaich + Freagair + Grad-fhreagairt + Beachd sam bith eile\? + Dèan gearan mu @%s + Dh’iarr %s leantainn ort + Lean %s ort + Chan eil dad an-seo. Tarraing a-nuas airson ath-nuadhachadh! + Chan eil dad an-seo. + Co-theannaich + Leudaich + Seall nas lugha dheth + Seall barrachd dheth + Briog air gus a shealltainn + Meadhanan falaichte + Susbaint fhrionasach + ’Ga bhrosnachadh le %s + \@%s + Ceadachasan + Iarrtasan leantainn + Àrainnean falaichte + Cleachdaichean bacte + Cleachdaichean mùchte + Comharran-lìn + Luchd-leantainn + A’ leantainn air + Prìnichte + Le freagairt + Postaichean + Tabaichean + Teachdaireachdan dìreach + Co-naisgte + Ionadail + Dachaigh + Dh’fhàillig leis an luchdadh suas. + Chan urrainn dhut an dà chuid dealbhan is videothan a cheangal ris an aon staid. + Tha feum air cead gus meadhanan a stòradh. + Tha feum air cead gus meadhanan a leughadh. + Cha b’ urrainn dhuinn am faidhle sin fhosgladh. + Cha ghabh an seòrsa de dh’fhaidhle seo a luchdadh suas. + Feumaidh faidhlichean fuaime a bhith nas lugha na 40MB. + Feumaidh faidhlichean video a bhith nas lugha na 40MB. + Feumaidh am faidhle a bhith nas lugha na 8MB. + Tha an staid ro fhada! + Cha deach leinn tòcan clàraidh a-steach fhaighinn. + Chaidh an t-ùghdarrachadh a dhiùltadh. + Thachair mearachd leis an ùghdarrachadh nach do dh’aithnich sinn. + Cha do lorg sinn brabhsair-lìn a chleachdadh sinn. + Dh’fhàillig leis an dearbhadh leis an ionstans ud. + Chuir thu a-steach àrainn-lìn mì-dhligheach + Chan fhaod seo a bhith falamh. + Thachair mearachd leis an lìonra! Thoir sùil air a’ cheangal agad is feuch ris a-rithist! + Thachair mearachd. + Ged nach eil an cunntas agad glaiste, tha sgioba %1$s dhen bheachd gum b’ fheàirrde thu lèirmheas a dhèanamh air na h-iarrtasan leantainn o na cunntasan seo a làimh. \ No newline at end of file diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml new file mode 100644 index 00000000..bbba46cf --- /dev/null +++ b/app/src/main/res/values-gl/strings.xml @@ -0,0 +1,514 @@ + + + Aviso sobre o contido + Visibilidade do toot + Toots programados + Borradores + Buscar + Rexeitar + Aceptar + Desfacer + Editar + Editar perfil + Gardar + Abrir editor + Agochar multimedia + Mencionar + Reactivar conversa + Acalar conversa + Reactivar %s + Acalar %s + Acalar as notificacións de %s + Reactivar as notificacións de %s + Reactivar %s + Desacalar + Acalar + Compartir + Facer foto + Engadir enquisa + Engadir multimedia + Abrir no navegador + Multimedia + Solicitudes de seguimento + Dominios agochados + Usuarias bloqueadas + Usuarias acaladas + Marcadores + Favoritos + Preferencias da conta + Preferencias + Perfil + Pechar + Intenta outra vez + TOOT! + TOOT + Eliminar e reescribir + Eliminar + Editar + Denunciar + Mostrar promocións + Agochar promocións + Desbloquear + Bloquear + Deixar de seguir + Seguir + Tes a certeza de que queres desconectar a conta %1$s\? + Desconectar + Conecta con Mastodon + Redactar + Máis + Eliminar favorito + Marcar + Favorito + Eliminar promoción + Promover + Responder + Resposta rápida + Comentarios adicionais\? + Denunciar a @%s + %s publicou agora + %s solicitou seguirte + %s seguiute + %s fixo favorito o teu toot + %s promoveu o teu toot + Nada por aquí. Arrastra hacia abaixo para actualizar! + Nada por aquí. + Pregar + Expandir + Amosar menos + Amosar máis + Click para ver + Multimedia agochado + Contido sensible + %s promoveu + \@%s + Licenzas + Anuncios + Toots programados + Borradores + Edita o teu perfil + Solicitudes de seguimento + Dominios agochados + Usuarias bloqueadas + Usuarias acaladas + Marcadores + Favoritos + Seguidoras + Segue + Fixado + Con resposta + Publicacións + Toot + Lapelas + Mensaxes directas + Federada + Local + Notificacións + Inicio + Erro ao enviar o toot. + Fallou a subida. + As imaxes e vídeo non poden engadirse simultáneamente a un mesmo estado. + Requírese o permiso de almacenaxe do multimedia. + Requírese o permiso de lectura do multimedia. + Non se puido abrir o ficheiro. + Non pode subirse ese tipo de ficheiro. + Os ficheiros de audio teñen que ser menores de 40MB. + Os ficheiros de vídeo teñen que ser menores de 40MB. + O ficheiro debe ser menor de 8MB. + O estado é demasiado longo! + Fallou a obtención do token de conexión. + A autorización foi rexeitada. + Aconteceu un erro non identificado de autorización. + Non se atopou un navegador para utilizar. + Fallou a autenticación nesta instancia. + O dominio escrito non é válido + Esto non pode estar baleiro. + Houbo un fallo na rede! Comproba a túa conexión e inténtao outra vez! + Algo fallou. + %1$s + Desubscribir + Subscribir + Eliminouse o toot para o que redactaches a resposta + Sen límite + Duración + Enquisa + Activar xestos de desprazamento para moverse entre lapelas + Motrar filtro das notificacións + Fallou a busca + Contas + A conta pertence a outro servidor. Queres enviar unha copia anónima da denuncia alí tamén\? + A denuncia vaise enviar á moderación do teu servidor. Podes engadir algunha explicación ou razón pola que estás denunciando a conta: + Fallou a obtención dos estados + Fallo ao realizar a denuncia + Reenviar a %s + Comentarios adicionais + Denuncia feita sobre @%s + Feito + Atrás + Continuar + + queda %d segundo + quedan %d segundos + + + queda %d minuto + quedan %d minutos + + + queda %d hora + quedan %d horas + + + queda %s día + quedan %s días + + Rematou unha enquisa creada por ti + Rematou unha enquisa na que votaches + Votar + pechada + remata en %s + + %s persoa + %s persoas + + + %s voto + %s votos + + %1$s • %2$s + Accións para a imaxe %s + Tes a certeza de que queres borrar permanentemente todas as notificacións\? + Redactar + Redactar Toot + Aplicar + Filtrar + Despexar + Listaxe + Elexir listaxe + Cancelos + Cancelo sen # + Engadir cancelo + Nome da lista + Enquisa con opcións: %1$s, %2$s, %3$s, %4$s; %5$s + Directo + Seguidoras + Non listado + Público + Marcado + Favorecido + Repetido + Sen descrición + Aviso sobre o contido: %s + Multimedia: %s + + acadouse o máximo de %1$d lapela + acadouse o máximo de %1$d lapelas + + %1$s, %2$s e %3$d máis + %1$s e %2$s + Favorecido por + Promovido por + + %s Promoción + %s Promocións + + + %1$s Favorito + %1$s Favoritos + + Fixar + Desafixar + A información inferior sobre a usuaria podería non estar completa. Preme para ver o perfil completo no navegador. + Usar hora absoluta + Contido + Etiqueta + engadir datos + Metadatos do perfil + CC-BY-SA 4.0 + CC-BY 4.0 + Con licenza Apache License (ver abaixo) + Tusky ten código e ferramentas dos seguintes proxectos de código aberto: + Retirar promoción + Promover para a audiencia orixinal + %1$s migrou a: + Bot + Fallou a descarga + Conxunto actual de emojis de Google + Conxunto de emojis estándar en Mastodon + Os emojis Blob coñecidos de Android 4.4-7.1 + O conxunto de emojis por defecto no sistema + Reiniciar + Máis tarde + Deberás reiniciar Tusky para aplicar os cambios + Require reiniciar app + Abrir toot + Expandir/Pregar tódolos estados + Realizando a busca… + Deberás descargar primeiro estos conxuntos de emojis + Por defecto no sistema + Estilo dos emoji + Copiado ao portapapeis + A túa instancia %s non ten emojis personalizados + Redactar + Gardouse unha copia do toot nos borradores + Envío cancelado + Enviando Toots + Erro ao enviar o toot + Enviando Toot… + Gardar borrador\? + Require que aprobes manualmente as seguidoras + Bloquear conta + Eliminar + Escribir descrición + + Describe para persoas con problemas de visión +\n(%d caracteres como máximo) + + Fallou establecemento do texto + Publicar coa conta %1$s + Eliminar conta da listaxe + Engadir conta á listaxe + Atopar persoas ás que segues + Editar a listaxe + Eliminar a listaxe + Renomear a listaxe + Crear unha listaxe + Non se puido eliminar a listaxe + Non se puido renomear a listaxe + Non se puido crear a listaxe + Cronoloxía da listaxe + Listaxes + Listaxes + Engadir unha nova conta Mastodon + Engadir conta + Frase a filtrar + Cando a palabra ou frase é só alfanumérica, só se aplicará se concorda a palabra completa + Palabra completa + Actualizar + Eliminar + Editar filtro + Engadir filtro + Conversas + Cronoloxías públicas + cargar máis + Respondendo a @%s + Multimedia + Expandir sempre toots marcados con avisos sobre o contido + Mostrar sempre contido sensible + Séguete + %ds + %dm + %dh + %dd + %dy + fai %ds + fai %dh + fai %dm + en %dd + en %dy + Seguimento solicitado + Anexos + Audio + Vídeo + Imaxes + Compartir ligazón ao toot + Compartir contido do toot + Perfil de Tusky + Informar de fallos e solicitar funcións: +\n https://github.com/tuskyapp/Tusky/issues + Web do proxecto: +\n https://tusky.app + Tusky é software libre e de código aberto. Está baixo a licenza GNU General Public License Version 3. Podes ver a licenza aquí: https://www.gnu.org/licenses/gpl-3.0.en.html + Desenvolta por Tusky + Tusky %s + Acerca de + Conta bloqueada + + %d nova interacción + %d novas interaccións + + %1$s e %2$s + %1$s, %2$s, e %3$s + %1$s, %2$s, %3$s e %4$d outras + %s mencionoute + Notificacións cando alguén a quen estás subscrita publica un novo toot + Novos toots + Notificacións cando rematan as enquisas + Enquisas + Notificacións cando os teus toots son marcados como favoritos + Favoritos + Notificacións cando os teus toots son promovidos + Promocións + Notificación acerca de solicitudes de seguimento + Solicitudes de seguimento + Notificacións acerca de novas seguidoras + Novas seguidoras + Notificación de novas mencións + Novas mencións + O máis grande + Grande + Medio + Pequeno + O máis pequeno + Tamaño do texto do estado + Só seguidoras + Non listado + Público + Abaixo + Arriba + Posición de navegación principal + Fallou a sincronización dos axustes + Publicando (sincronizado co servidor) + Marcar sempre multimedia como sensible + Privacidade por defecto da publicación + Porto proxy HTTP + Servidor proxy HTTP + Activar proxy HTTP + Proxy HTTP + Proxy + Descarga vista previa do multimedia + Mostar respostas + Mostrar promocións + Lapelas + Filtros na cronoloxía + Animar emojis personalizados + Mostra gradientes coloridos para multimedia oculto + Animar avatares GIF + Mostra marca para bots + Idioma + Agochar o botón redactar ao desprazar + Usar lapelas personalizadas de Chrome + Navegador + Usar deseño do sistema + Automático ao solpor + Negro + Claro + Escuro + Filtros + Cronoloxías + Decorado da app + Aparencia + alguén a quen eu siga publique un toot + rematen as enquisas + marquen un toot meu como favorito + promocionen un dos meus toots + soliciten seguirme + me sigan + me mencionen + Notifícame cando + Notificar coa luz + Notificar con vibración + Nofiticar cun ton + Alertas + Notificacións + Notificacións + Directo: Visible só polas persoas mencionadas + Só seguidoras: visible só polas seguidoras + Non listado: non mostrar en cronoloxías públicas + Público: Publicar en cronoloxías públicas + Agochar notificacións + Acalar a @%s\? + Bloquear @%s\? + Agochar todo o dominio + Tes a certeza de querer bloquear a todo %s\? Non verás o contido dese dominio en ningunha cronoloxía pública ou nas notificacións. As túas seguidoras nese dominio serán eliminadas. + Eliminar e reescribir este toot\? + Eliminar este toot\? + Deixar de seguir esta conta\? + Revogar a solicitude de seguimento\? + Descargar + Subindo… + Rematando a subida multimedia + Aquí podes escribir o enderezo ou dominio de calquera instancia, como mastodon.social, icosahedron.website, social.techncs.de, e máis! +\n +\nSe aínda non tes unha conta, podes escribir o nome da instancia á que desexas unirte e crear unha conta nela. +\n +\nUnha instancia é o lugar onde se hospeda a túa conta, pero podes comunicarte facilmente e seguir a persoas noutras instancias como se estiveses alí. +\n +\nPodes atopar máis información en joinmastodon.org. + Conectando… + Que é unha instancia\? + Cabeceira + Avatar + Responder… + Sen resultados + Buscar… + Bio + Nome público + Aviso sobre o contido + Que contas\? + Borrador eliminado + Fallou a carga da información da Resposta + Borradores antigos + Os borradores en Tusky foron redeseñados para ser máis rápidos, amigables para a usuaria e con menos fallos. +\nAínda podes acceder aos antigos borradores a través do botón na pantalla de novos borradores, pero eliminarémolo en futuras actualizacións! + Fallou o envío do toot! + Tes a certeza de querer eliminar a listaxe %s\? + + Non podes subir máis de %1$d anexo multimedia. + Non podes subir máis de %1$d anexos multimedia. + + Agochar estatísticas cuantitativas nos perfís + Agochar estatísticas cuantitativas nas publicacións + Limitar notificacións da cronoloxía + Revisar Notificacións + Agocharemos algunha información que podería afectar ao teu benestar mental. Esto inclúe: +\n +\n- Notificacións acerca de Favoritos/Promocións/Seguimentos +\n- Número de Favoritos/Promocións nos toots +\n- Estatísticas de Seguidoras/Publicacións nos perfís +\n +\nAs notificacións tipo push non estarán afectadas, mais podes revisar as preferencias de notificacións manualmente. + Gardado! + Nota privada acerca desta conta + Benestar + Agochar o título da barra de ferramentas superior + Amosar diálogo de confirmación antes de promover + Amosar vista previa das ligazóns nas cronoloxías + Mastodon ten un intervalo mínimo de 5 minutos para as programacións. + Non hai anuncios. + Non tes estados programados. + Non tes borradores. + Erro ao buscar publicación %s + Editar + Opción %d + Múltiples opcións + Engadir opción + 7 días + 3 días + 1 día + 6 horas + 1 hora + 30 minutos + 5 minutos + En que instancia\? + Resposta enviada correctamente. + Enviado! + %s visible + Usuaria reactivada + Usuaria desbloqueada + Enviado! + Compartir multimedia en… + Compartir toot en… + Compartir URL do toot a… + Descargando multimedia + Descargar multimedia + Compartir como … + Abrir como %s + Copiar ligazón + Descargando %1$s + Abrir multimedia #%d + Ligazóns + Mencións + Cancelos + Mostrar favoritos + Mostrar promocións + Abrir autor da promoción + Cancelos + Mencións + Ligazóns + Engadir lapela + Restablecer + Programar Toot + Teclado Emoji + Aínda que a túa conta non está bloqueada, a administración de %1$s opina que debes revisar manualmente as peticións de seguimento destas contas. + \ No newline at end of file diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 4591a85e..a99791f9 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -280,7 +280,9 @@ असूचीबद्ध सार्वजनिक विषय वस्तु चेतावनी: %s - अधिकतम %1$d टैब तक पहुंच गऐ + + अधिकतम %1$d टैब तक पहुंच गऐ + %1$s, %2$s तथा %3$d अन्य लोग %1$s तथा %2$s %1$s @@ -310,8 +312,10 @@ टूट भेजने में त्रुटि हटाएँ हटा दें - दृष्टिहीन लोगों के लिए वर्णन करें -\n(%d वर्ण सीमा) + + दृष्टिहीन लोगों के लिए वर्णन करें +\n(%d वर्ण सीमा) + कैप्शन सेट करने में विफल %1$s खाते के साथ पोस्ट कर रहे सूची से खाता निकालें diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 38dd5130..9c07a49d 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -182,7 +182,7 @@ HTTP proxy szerver HTTP Proxy port Tülkök alapértelmezett láthatósága - Minden média szenzitívnek jelölése + Minden média kényesnek jelölése A beállítások szinkronizálása nem sikerült Nyilvános Listázatlan @@ -205,7 +205,10 @@ %1$s, %2$s, %3$s és még %4$d %1$s, %2$s meg %3$s %1$s és %2$s - %d új interakció + + %d új interakció + %d új interakció + Zárolt fiók Rólunk Tusky %s @@ -228,7 +231,7 @@ Követés kérelmezve Követ téged - Mindig mutassa a szenzitív tartalmat + Mindig mutassa a kényes tartalmat Média több betöltése Fiók hozzáadása @@ -273,7 +276,10 @@ Megtolta Kedvencnek jelölte %1$s és %2$s - elérted a fülek maximális számát (%1$d) + + elérted a fülek maximális számát (%1$d) + elérted a fülek maximális számát (%1$d) + Nincs leírás Nyilvános @@ -338,8 +344,10 @@ Fiók eltávolítása a listából Tülkölés %1$s fiókkal Cím beállítása nem sikerült - Leírás látássérülteknek -\n(%d karakter korlát) + + Leírás látássérülteknek +\n(%d karakter korlát) + Cím beállítása Minden követődet külön engedélyezned kell Minden tülk kibontása/összecsukása @@ -484,7 +492,10 @@ \nTovábbra is elérheted a régi piszkozataidat egy gombbal az új piszkozatok képernyőjén, de ezeket egy későbbi frissítésben el fogjuk törölni! Ez a tülk nem küldődött el! Tényleg le akarod törölni a %s listát\? - Nem tölthetsz fel %1$d médiacsatolmányból többet. + + Nem tölthetsz fel %1$d médiacsatolmányból többet. + Nem tölthetsz fel %1$d médiacsatolmányból többet. + Profilok mérőszámainak elrejtése Tülkök mérőszámainak elrejtése Idővonali értesítések korlátozása @@ -506,4 +517,7 @@ %s épp tülkölt Jóllét Egyedi emojik animálása + Leiratkozás + Feliratkozás + Bár a fiókod nincs zárolva, a %1$s csapata úgy gondolta, hogy ezen fiókok követési kérelmeit átnéznéd. \ No newline at end of file diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index 6f8fe739..7a80ba9c 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -243,7 +243,10 @@ %1$s, %2$s, %3$s og %4$d til viðbótar %1$s, %2$s og %3$s %1$s og %2$s - %d nýjar aðgerðir + + %d ný aðgerð + %d nýjar aðgerðir + Læstur notandaaðgangur Tusky %s Keyrir á Tusky @@ -298,8 +301,10 @@ Fjarlægja notandaaðganginn af listanum Sendi með notandaaðgangnum %1$s Ekki tókst að setja skýringatexta - Lýstu þessu fyrir sjónskerta -\n(hámark %d stafir) + + Lýstu þessu fyrir sjónskerta +\n(hámark %d stafir) + Setja skýringatexta Fjarlægja Læsa notandaaðgangi @@ -349,7 +354,10 @@ %1$s %1$s og %2$s %1$s, %2$s og %3$d til viðbótar - hámarksfjölda %1$d flipa náð + + hámarksfjölda %1$d flipa náð + hámarksfjölda %1$d flipa náð + Myndefni: %s Aðvörun vegna efnis: %s Engin lýsing @@ -439,7 +447,7 @@ %d dagar eftir Vistað! - Þí eigin einkaathugasemd um þennan aðgang + Þín eigin einkaathugasemd um þennan aðgang Fela titil á verkfærastikunni efst Birta staðfestingarglugga áður en endurbirting fer fram Birta forskoðun tengla á tímalínum @@ -459,12 +467,48 @@ Fela tilkynningar Þagga niður í @%s\? Loka á @%s\? - Hætta að þagga niður í samtölum - Þagga niður í samtölum + Hætta að þagga niður í samtali + Þagga niður í samtali Afþagga %s Þagga tilkynningar frá %s Afþagga tilkynningar frá %s Afþagga %s %s bað um að fylgjast með þér Tilkynningar + Gerð draga í Tusky hefur verið endurhönnuð til að verða fljótlegri, notendavænni og gallalaus. +\n Þú getur áfram nýtt eldri drög í gegnum sérstakan hnapp í glugganum fyrir drög, en sá eiginleiki verður fjarlægður í framtíðaruppfærslu! + Sumar upplýsingar sem gætu haft áhrif á andlega vellíðan þína verða faldar. Þetta hefur áhrif á: +\n +\n - Eftirlæti/Endurbirtingar/Tilkynningar um fylgjendabeiðnir +\n - Eftirlæti/Talningu á endurbirtingum tísta +\n - Fylgjendur/Tölfræði færslna í notendasniðum +\n +\n Þetta hefur ekki áhrif á ýti-tilkynningar, en þú getur yfirfarið handvirkt kjörstillingar þínar varðandi tilkynningar. + Segja upp áskrift + Gerast áskrifandi + Hreyfa sérsniðin tjáningartákn + Tístið sem þú gerðir drög að svari við hefur veriið fjarlægt + Eyddi drögum + Mistókst að hlaða inn svarupplýsingum + Eldri drög + Mistókst að senda þetta tíst! + Viðhengi + Hljóð + Ertu viss um að þú viljir eyða %s listanum\? + Ótiltekið + Tímalengd + + Þú getur ekki sent inn fleiri en %1$d myndefnisviðhengi. + Þú getur ekki sent inn fleiri en %1$d myndefnisviðhengi. + + Fela magntölfræði notendasniða + Fela magntölfræði færslna + Takmarka tilkynningar á tímalínu + Yfirfara tilkynningar + Vellíðan + Tilkynningar þegar einhver sem þú ert áskrifandi að hefur birt nýtt tíst + Ný tíst + einhver sem ég er áskrifandi að birti nýtt tíst + %s sendi inn rétt í þessu + Jafnvel þótt aðgangurinn þinn sé ekki læstur, fannst starfsfólki %1$s að þú gætir viljað yfirfara handvirkt fylgjendabeiðnir frá þessum aðgöngum. \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index dbaae3f2..b940b5f3 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -216,7 +216,9 @@ %1$s, %2$s, %3$s e %4$d altri %1$s, %2$s e %3$s %1$s e %2$s - %d nuove interazioni + + %d nuove interazioni + Account bloccato A proposito Tusky %s @@ -279,7 +281,9 @@ Rimuovi un account dalla lista Pubblicando con l\'account %1$s Impostazione del sottotitolo non riuscita - Descrivi per ipovedenti\n(limite di %d caratteri) + + Descrivi per ipovedenti\n(limite di %d caratteri) + Inserisci descrizione Rimuovi Blocca account @@ -336,7 +340,9 @@ %1$s %1$s e %2$s %1$s, %2$s ed altri %3$d - limite massimo di %1$d tab raggiunto + + limite massimo di %1$d tab raggiunto + Media: %s Contenuto sensibile: %s @@ -492,5 +498,7 @@ Nuovi toots qualcuno a cui sono iscritto ha pubblicato un nuovo toot %s appena pubblicato - Non puoi caricare più di %1$d allegati multimediali. + + Non puoi caricare più di %1$d allegati multimediali. + \ No newline at end of file diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index e019ff61..19ad213c 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -206,7 +206,9 @@ %1$sさん、%2$sさん、%3$sさんと他%4$d人 %1$sさん、%2$sさん、%3$sさん %1$sさん、%2$sさん - %d件の新しい通知 + + %d件の新しい通知 + 非公開アカウント このアプリについて Tusky %s @@ -258,7 +260,9 @@ リスト名の変更 %1$sで投稿 説明の設定に失敗しました - 視覚障害者のための説明 (%d文字まで) + + 視覚障害者のための説明 (%d文字まで) + 説明を設定 消去 アカウントをロック @@ -310,7 +314,9 @@ ブーストした人物 お気に入りした人物 - タブは %1$d 個が上限です + + タブは %1$d 個が上限です + 公開 未収載 @@ -456,4 +462,6 @@ %s人 Mastodonにおける予約までの最小間隔は5分です。 + %sさんがトゥートしました + お知らせ \ No newline at end of file diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 454f3208..6b6fff2c 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -230,7 +230,9 @@ %1$s, %2$s, %3$s, 그 외 %4$d개 %1$s님, %2$s님, %3$s님 %1$s와 %2$s - %d개의 새로운 알림이 있습니다 + + %d개의 새로운 알림이 있습니다 + 계정 잠김 이 앱에 관하여 Tusky %s @@ -293,8 +295,10 @@ 리스트에서 계정 삭제 %1$s로서 포스팅 미디어에 대한 설명을 추가할 수 없습니다 - 시각 장애인을 위한 설명 -\n(%d글자 작성 가능) + + 시각 장애인을 위한 설명 +\n(%d글자 작성 가능) + 설명 추가 삭제 계정 잠금 @@ -350,7 +354,9 @@ %1$s %1$s와 %2$s %1$s, %2$s 외 %3$d명 - 최대 탭 수 %1$d에 도달했습니다 + + 최대 탭 수 %1$d에 도달했습니다 + 미디어: %s 열람주의: %s 설명 없음 diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 85e1b2ea..2299cce8 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -18,7 +18,7 @@ Er is toestemming nodig om media op te slaan. Afbeeldingen en video\'s kunnen niet allebei aan dezelfde toot worden toegevoegd. Uploaden mislukt. - Fout tijdens verzenden toot, + Fout tijdens verzenden toot. Start Meldingen Lokaal @@ -53,7 +53,7 @@ %s markeerde jouw toot als favoriet %s volgt jou Rapporteer @%s - Extra opmerkingen + Extra opmerkingen\? Snelle reactie Reageren Boosten @@ -215,7 +215,10 @@ %1$s, %2$s, %3$s en %4$d anderen %1$s, %2$s en %3$s %1$s en %2$s - %d nieuwe interacties + + %d nieuwe interactie + %d nieuwe interacties + Besloten account Over Tusky %s @@ -259,7 +262,9 @@ Tijdlijn lijst Aan het publiceren met account %1$s Toevoegen van beschrijving mislukt - Omschrijf dit voor mensen met een visuele beperking\n(tekenlimiet is %d) + + Omschrijf dit voor mensen met een visuele beperking\n(tekenlimiet is %d) + Beschrijving toevoegen Verwijderen Account besloten maken @@ -316,7 +321,10 @@ %1$s %1$s en %2$s %1$s, %2$s en %3$d meer - maximum van %1$d tabs bereikt + + maximum van %1$d tab bereikt + maximum van %1$d tabs bereikt + Media: %s Inhoudswaarschuwing: %s @@ -390,16 +398,16 @@ Poll met keuzes: %s, %s, %s, %s; %s Acties voor afbeelding %s - %d dag over - %d dagen over + %d dag te gaan + %d dagen te gaan - %d uur over - %d uur over + %d uur te gaan + %d uur te gaan - %d minuut over - %d minuten over + %d minuut te gaan + %d minuten te gaan %d seconde over @@ -448,9 +456,64 @@ Zoeken mislukt Poll Fout tijdens opzoeken toot %s - Je hebt nog geen concepten - Je hebt nog geen ingeplande toots + Je hebt nog geen concepten. + Je hebt nog geen ingeplande toots. Om in te plannen moet je in Mastodon een minimum interval van 5 minuten gebruiken. Volgverzoeken Hashtags + Bijlagen + volgverzoek verstuurd + Afmelden + Abonneren + De toot waarvoor jij een reactie had opgesteld, is verwijderd + Het versturen van deze toot is mislukt! + Weet je zeker dat je de lijst %s wilt verwijderen\? + + Je kan niet meer dan %1$d mediabijlage uploaden. + Je kan niet meer dan %1$d mediabijlagen uploaden. + + Meldingen op tijdlijn beperken + Opgeslagen! + Jouw eigen opmerking over dit account + Welzijn + De titel van de bovenste statusbalk verbergen + Vraag voor het boosten van een toot een bevestiging + Linkpreviews in tijdlijnen weergeven + Er zijn geen aankondigingen. + Oneindig + Looptijd + Swipebewegingen om tussen tabs te schakelen inschakelen + + %s persoon + %s personen + + Hashtag toevoegen + Geluid + Meldingen wanneer iemand waar je op bent geabonneerd een nieuwe toot plaatst + Nieuwe toots + Meldingen over volgverzoeken + Onder + Boven + Lokale emojis animeren + Kleurverloop weergeven voor verborgen media + iemand waar ik op ben geabonneerd heeft een nieuwe toot geplaatst + Meldingen verbergen + \@%s negeren\? + \@%s blokkeren\? + Gesprek niet meer negeren + Gesprek negeren + %s niet meer negeren + Meldingen van %s negeren + Meldingen van %s niet meer negeren + %s niet meer negeren + %s heeft net een toot geplaatst + %s verzoekt u te volgen + Aankondigingen + Meldingen beoordelen + Concept verwijderd + Kwantitatieve statistieken voor toots verbergen + Laden van reactie-informatie mislukt + Oude concepten + Kwantitatieve statistieken in profielen verbergen + Hoofd navigatiepositie \ No newline at end of file diff --git a/app/src/main/res/values-no-rNB/strings.xml b/app/src/main/res/values-no-rNB/strings.xml index 36e2fc9c..13a6e7e5 100644 --- a/app/src/main/res/values-no-rNB/strings.xml +++ b/app/src/main/res/values-no-rNB/strings.xml @@ -34,7 +34,7 @@ Favoritter Dempede brukere Blokkerte brukere - Forespørsler om følgen + Følgeforespørsler Endre profilen din Kladder Lisenser @@ -83,7 +83,7 @@ Favoritter Dempede brukere Blokkerte brukere - Forespørsler om følging + Følgeforespørsler Media Åpne i nettleser Legg til media @@ -202,7 +202,10 @@ %1$s, %2$s, %3$s og %4$d anre %1$s, %2$s, og %3$s %1$s og %2$s - %d nye interaksjoner + + %d ny interaksjon + %d nye interaksjoner + Låst konto Om Tusky %s @@ -259,8 +262,10 @@ om %dy Poster med konto %1$s Klarte ikke å sette bildetekst - Beskriv for de med nedsatt synsevne -\n(maks %d tegn) + + Beskriv for de med nedsatt synsevne +\n(maks %d tegn) + Sett bildetekst Slett Lås konto @@ -317,7 +322,10 @@ %1$s %1$s og %2$s %1$s, %2$s og %3$d fler - grensen på %1$d faner er nådd + + grensen på %1$d fane er nådd + grensen på %1$d faner er nådd + Media: %s Innholdsadvarsel: %s Ingen beskrivelse @@ -444,8 +452,8 @@ %s personer Varsler om følgeforespørsler - Forespørsler om følging - følging forespurt + Følgeforespørsler + Følgeforespørsel sendt Dempe @%s\? Blokkere @%s\? Fjern demping av samtale @@ -483,7 +491,10 @@ Nye toots noen jeg følger publiserer en ny toot %s tootet akkurat - Du kan ikke laste opp flere enn %1$d mediavedlegg. + + Du kan ikke laste opp flere enn %1$d mediavedlegg. + Du kan ikke laste opp flere enn %1$d mediavedlegg. + Uendelig Varighet Er du sikker på at du vil slette listen %s\? @@ -499,4 +510,5 @@ Animer egendefinerte emojis Avslutt abonnementet Abonner + Selv om kontoen din ikke er låst, har %1$s administratorer markert disse følgeforespørsler for manuell godkjenning. \ No newline at end of file diff --git a/app/src/main/res/values-oc/strings.xml b/app/src/main/res/values-oc/strings.xml index a35c08ab..c3647aac 100644 --- a/app/src/main/res/values-oc/strings.xml +++ b/app/src/main/res/values-oc/strings.xml @@ -185,7 +185,9 @@ %1$s, %2$s, %3$s e %4$d mai %1$s, %2$s e %3$s %1$s e %2$s - %d interaccions nòvas + + %d interaccions nòvas + Compte blocat A prepaus Tusky es programa gratuït, liure e de còdi dobèrt. @@ -323,8 +325,10 @@ Cercar lo monde que seguètz Ajustar un compte a la lista Suprimir aqueste compte de la lista - Descriure pels mal vesents -\n(%d caractèrs maximum) + + Descriure pels mal vesents +\n(%d caractèrs maximum) + CC-BY 4.0 CC-BY-SA 4.0 @@ -340,7 +344,9 @@ %1$s %1$s e %2$s %1$s, %2$s e %3$d mai - nombre maximum d’onglets %1$d atengut + + nombre maximum d’onglets %1$d atengut + Mèdia : %s Avertiment : %s Cap de descripcion diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 1ea10dc1..2253a9ca 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -182,7 +182,9 @@ %1$s, %2$s, %3$s i %4$d innych %1$s, %2$s, i %3$s %1$s i %2$s - %d nowych powiadomień + + %d nowych powiadomień + Konto zablokowane O programie Tusky jest wolnym i otwartoźródłowym oprogramowaniem. Jest on dostępny na licencji GNU General Public License w wersji trzeciej. Możesz przeczytać przetłumaczoną treść licencji tutaj @@ -334,8 +336,10 @@ Szukaj osób, które śledzisz Dodaj konto do listy Usuń konto z listy - 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 Bot CC-BY 4.0 @@ -360,7 +364,9 @@ %1$s %1$s i %2$s %1$s, %2$s i %3$d innych - maksymalna liczba zakładek (%1$d) osiągnięta + + maksymalna liczba zakładek (%1$d) osiągnięta + Media: %s Ostrzeżenie o zawartości: %s Brak opisu diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index bcf47ec4..97b6566a 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -202,7 +202,9 @@ %1$s, %2$s, %3$s e %4$d outros %1$s, %2$s, e %3$s %1$s e %2$s - %d novas interações + + %d novas interações + Conta trancada Sobre Tusky %s @@ -328,8 +330,10 @@ Pesquisar pessoas que você segue Adicionar conta à lista Remover conta da lista - Descrever para deficientes visuais -\n(até %d caracteres) + + Descrever para deficientes visuais +\n(até %d caracteres) + CC-BY 4.0 CC-BY-SA 4.0 As informações abaixo podem refletir incompletamente o perfil do usuário. Toque aqui para abrir o perfil completo no navegador. @@ -346,7 +350,9 @@ %1$s %1$s e %2$s %1$s, %2$s e %3$d outros - excedeu o máximo de %1$d abas + + excedeu o máximo de %1$d abas + Mídia: %s Aviso de Conteúdo: %s Sem descrição @@ -481,7 +487,9 @@ A função de rascunhos no Tusky foi totalmente redesenhada para ser mais rápida, mais fácil e com menos erros. \nÉ possível acessar rascunhos antigos através de um botão na tela de novos rascunhos, mas serão removidos numa futura atualização! Rascunhos antigos - Não é possível anexar mais de %1$d arquivos de mídia. + + Não é possível anexar mais de %1$d arquivos de mídia. + Ocultar status dos perfis Ocultar status dos toots Limitar notificações da linha do tempo diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 6724cafe..6a2c5230 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -16,7 +16,7 @@ Файл не может быть открыт. Необходимо разрешение на чтение медиаконтента. Необходимо разрешение для хранения медиаконтента. - Изображения и видео не могут быть прикрекплены к статусу одновременно. + Изображения и видео не могут быть прикреплены к статусу одновременно. Загрузка не удалась. Ошибка при отправке поста. Главная @@ -43,14 +43,14 @@ Чувствительный контент Медиа скрыто Нажмите для просмотра - Развернуть - Свернуть + Показать больше + Показать меньше Развернуть Свернуть Ничего нет. Ничего нет. Потяните вниз, чтобы обновить! - %s продвинул(а) ваш статус - %s понравился ваш статус + %s продвинул(а) вашу запись + %s понравился ваша запись %s подписался(-лась) на вас Пожаловаться на @%s Дополнительные комментарии? @@ -59,8 +59,8 @@ Продвинуть Убрать продвижение Нравится - Не нравится - Развернуть + Убрать из избранного + Больше Написать Войти Выйти @@ -70,7 +70,7 @@ Заблокировать Разблокировать Скрыть продвижения - Показывать продвижения + Показать продвижения Пожаловаться Удалить Удалить и исправить @@ -94,10 +94,10 @@ Отменить глушение Упомянуть Скрыть медиаконтент - Нарисовать + Открыть drawer Сохранить Редактировать профиль - Изменить + Редактировать Отменить Принять Отклонить @@ -112,7 +112,7 @@ Хэштеги Перейти к автору Показывать продвижения - Показать, кому нравится + Показать избранное Хэштеги Упоминания Ссылки @@ -123,12 +123,12 @@ Поделиться как … Скачать медиафайл Скачивание медиафайла - Поделиться ссылкой на статус… - Поделиться статусом… + Поделиться ссылкой на запись… + Поделиться записью… Поделиться медифайлом… Отправить! Пользователь разблокирован - Глушение снято + Пользователь разглушен Отправлено! Ответ успешно отправлен. Какой узел? @@ -143,18 +143,20 @@ Заголовок Что такое узел? Соединение… - Здесь можно ввести адрес или домен любого узла, например, mastodon.social, icosahedron.website, social.tchncs.de и других!\n\nЕсли у вас еще нет аккаунта, введите адрес узла, на котором хотите зарегистрироваться, и создайте аккаунт.\n\n - Узел - это то место, где размещен ваш аккаунт, но вы можете взаимодействовать с пользователями других узлов, как будто вы находитесь на одном сайте.\n - \n - Чтобы получить больше информации посетите joinmastodon.org. - + Здесь можно ввести адрес или домен любого узла, например, mastodon.social, icosahedron.website, social.tchncs.de и других! +\n +\nЕсли у вас ещё нет аккаунта, введите адрес узла, на котором хотите зарегистрироваться, и создайте аккаунт. +\n +\n Узел - это то место, где размещён ваш аккаунт, но вы можете взаимодействовать с пользователями других узлов, как будто вы находитесь на одном сайте. +\n +\n Чтобы получить больше информации посетите joinmastodon.org. Завершается загрузка медиаконтента Загружается… Скачать Отменить запрос на подписку? Отписаться от этого аккаунта? - Удалить статус? - Удалить статус и превратить его в черновик? + Удалить запись\? + Удалить запись и превратить её в черновик\? Публичный: Показать в публичных лентах Скрытый: Не показывать в лентах Приватный: Показать только подписчикам @@ -162,10 +164,10 @@ Push-уведомления Push-уведомления Предупреждения - Звуковые уведомления - Использовать вибрацию - Световые уведомления - Уведомлять когда… + Уведомлять звуком + Уведомлять вибрацией + Уведомлять светом + Уведомлять когда упомянули подписались мои посты продвинули @@ -176,7 +178,7 @@ Фильтры Тёмная Светлая - Черная + Чёрная Автоматическая (по времени) Как в системе Браузер @@ -220,7 +222,12 @@ %1$s, %2$s, %3$s и %4$d других %1$s, %2$s, и %3$s %1$s и %2$s - Новых событий: %d + + Новое событие: %d + Новые события: %d + Новых событий: %d + Новых событий: %d + Закрытый аккаунт О приложении Tusky %s @@ -305,7 +312,9 @@ Удалить аккаунт из списка Отправка от имени %1$s Не удалось добавить подпись - Описание для слабовидящих\n(не более %d символов) + + Описание для слабовидящих\n(не более %d символов) + Добавить подпись Удалить Закрыть аккаунт @@ -350,7 +359,7 @@ Открепить Закрепить - %1$s Понравилось + %1$s Понравился %1$s Понравилось %1$s Понравилось %1$s Понравилось @@ -366,7 +375,12 @@ %1$s %1$s и %2$s %1$s, %2$s и ещё %3$d - достигнут лимит в %1$d вкладок + + достигнут лимит в %1$d вкладку + достигнут лимит в %1$d вкладок + достигнут лимит в %1$d вкладок + достигнут лимит в %1$d вкладок + Медиафайл: %s @@ -423,9 +437,9 @@ Скрытые домены Заглушить %s %s показывается - Вы уверены, что хотите заблокировать %s целиком\? Вы перестанете видеть посты с того узла во всех публичных лентах и уведомлениях. Все ваши подписчики с того домена будут удалены. + Вы уверены, что хотите заблокировать %s целиком\? Вы перестанете видеть посты из того домена во всех публичных лентах и уведомлениях. Все ваши подписчики из того домена будут удалены. Скрыть узел целиком - завершившиеся опросы + опросы завершились Анимировать GIF-аватары Слово целиком Если слово или фраза состоит только из букв и цифр, будет учитываться полное совпадение @@ -455,10 +469,10 @@ Множественный выбор Вариант %d Изменить - Отложенные записи + Запланированные записи Редактировать - Отложенные записи - Отложить запись + Запланированные записи + Запланировать запись Сброс Закладки Добавить в закладки @@ -470,10 +484,10 @@ Аудиофайлы должны быть меньше 40МБ. Ошибка поиска поста %s У вас нет черновиков. - У вас нет запланированный постов. + У вас нет запланированных постов. Минимальный интервал планирования в Mastodon составляет 5 минут. - Показвать диалог подтверждения перед продвижением - Показывать предосмотр ссылок в лентах + Показывать диалог подтверждения перед продвижением + Показывать предпросмотр ссылок в лентах Включить переключение между вкладками смахиванием %s человек @@ -487,7 +501,7 @@ Заглушить @%s\? Заблокировать @%s\? Показать обсуждение - Скрыть обсуждение + Заглушить обсуждение запрос на подписку от %s Тэги Добавить тэг @@ -497,24 +511,50 @@ Расположение панели навигации Отменить глушение %s Скрыть уведомления - Заблокировать уведомления от %s + Заглушить уведомления от %s Получать уведомления от %s Разблокировать %s Сохранено! Ваша личная заметка об этой учётной записи - Скрыть заголовок в верхней панели + Скрыть заголовок верхней панели Объявлений нет. Объявления - "Некоторая информация, которая может повлиять на ваше психическое благополучие, будет скрыта. Это включает в себя: + "Информация, могущая повлиять на ваше психическое благополучие, будет скрыта. Она включает: \n \n - Избранное/Продвижение/Уведомления подписок -\n - Избранное/Продвижение счета на тутах -\n - Статистика подписчиков/публикаций в профилях -\n +\n - Избранное/Счётчики продвижения постов +\n - Статистика подписчиков/постов в профилях +\n \n На push-уведомления это не повлияет, но вы можете просмотреть настройки уведомлений вручную." Благосостояние Неопределённая Продолжительность Вложения Аудио + %s только что опубликовал(а) + Ваша учётная запись не заблокирована, но персонал %1$s подумал, что вы можете захотеть вручную просмотреть запросы на отслеживание от этих учётных записей. + + Вы не можете загрузить более %1$d мультимедийного вложения. + Вы не можете загрузить более %1$d мультимедийных вложений. + Вы не можете загрузить более %1$d мультимедийных вложений. + Вы не можете загрузить более %1$d мультимедийных вложений. + + Скрыть количественную статистику по сообщениям + Отписаться + Подписаться + Пост, на который вы написали ответ, был удалён + Не удалось загрузить информацию об ответе + Функция черновика в Tusky была полностью переработана, чтобы сделать её более быстрой, удобной и стабильной. +\nВы по-прежнему можете получить доступ к своим старым черновикам с помощью кнопки на экране новых черновиков, но они будут удалены в будущем обновлении! + Старые черновики + Черновик удалён + Этот пост не удалось отправить! + Вы действительно хотите удалить список %s\? + Скрыть количественную статистику по сообщениям + Ограничить уведомления на временной шкале + Просмотр уведомлений + Уведомления, когда кто-то, на кого вы подписаны, опубликовал новую запись + Новые записи + Анимировать собственные эмодзи + кто-то, на кого я подписан, опубликовал новую запись \ No newline at end of file diff --git a/app/src/main/res/values-sa/strings.xml b/app/src/main/res/values-sa/strings.xml index fd79b509..271136af 100644 --- a/app/src/main/res/values-sa/strings.xml +++ b/app/src/main/res/values-sa/strings.xml @@ -247,7 +247,9 @@ टस्की %s विज्ञप्तिः कपाटितव्यक्तिविवरणलेखः - %d नवपरस्परक्रियाः + + %d नवपरस्परक्रियाः + %1$s च %2$s च %1$s, %2$s, तथैव %3$s %1$s, %2$s, %3$s तथा च %4$d अन्येऽपि @@ -328,8 +330,10 @@ लेखा अवरुध्यताम् नश्यताम् शीर्षकवाक्यं लिख्यताम् - दृष्ट्यां येषां समस्याऽस्ति तेषांं कृते विवरणम् -\n(%d परिमिता न्यूनाक्षरसङ्ख्या) + + दृष्ट्यां येषां समस्याऽस्ति तेषांं कृते विवरणम् +\n(%d परिमिता न्यूनाक्षरसङ्ख्या) + शीर्षकवाक्यं लेखितुमशक्यम् %1$s लेखया प्रकटनं क्रियते सूच्याः लेखा नश्यताम् @@ -449,7 +453,9 @@ विवरणं नास्ति विषयपूर्वसतर्कता: %s सामग्र्यः %s - अधिकतमपीठिकासङ्ख्या %1$d भूता + + अधिकतमपीठिकासङ्ख्या %1$d भूता + %1$s, %2$s तथा %3$d अन्येऽपि %1$s तथैव %2$s निम्नमित्रस्य प्रीतिः diff --git a/app/src/main/res/values-si/strings.xml b/app/src/main/res/values-si/strings.xml new file mode 100644 index 00000000..d9993b2f --- /dev/null +++ b/app/src/main/res/values-si/strings.xml @@ -0,0 +1,17 @@ + + + %1$s සහ %2$s + %1$s + පෙරහන + යොදන්න + ආපසු + ගිණුම් + විනාඩි 5 + විනාඩි 30 + හෝරා 6 + හෝරා 1 + දින 3 + දින 1 + දින 7 + සංස්කරණය + \ No newline at end of file diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index 3f969334..f910db4e 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -204,7 +204,9 @@ %1$s, %2$s, %3$s in %4$d ostali %1$s, %2$s in %3$s %1$s in %2$s - %d novih interakcij + + %d novih interakcij + Zaklenjen račun O aplikaciji Tusky %s @@ -258,8 +260,10 @@ Odstrani račun iz seznama Objavljanje z računom %1$s Opisa ni bilo mogoče nastaviti - Opišite za slabovidne -\n(omejitev znakov - %d) + + Opišite za slabovidne +\n(omejitev znakov - %d) + Nastavi opis Odstrani Zakleni račun @@ -314,7 +318,9 @@ %1$s %1$s in %2$s %1$s, %2$s in %3$d več - doseženih maksimalnih %1$d zavihkov + + doseženih maksimalnih %1$d zavihkov + Mediji: %s Opozorila o vsebini: %s Brez opisa diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 160b2ee5..9735f512 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -217,7 +217,9 @@ %1$s, %2$s, %3$s och %4$d andra %1$s, %2$s, och %3$s %1$s och %2$s - %d nya interaktioner + + %d nya interaktioner + Låst konto Om Tusky %s @@ -280,7 +282,9 @@ Ta bort kontot från listan Inlägg med kontot %1$s Misslyckades med att ange bildtext - Beskriv för synskadade\n(%d teckengräns) + + Beskriv för synskadade\n(%d teckengräns) + Ange bildtext Ta bort Lås konto @@ -337,7 +341,9 @@ %1$s %1$s och %2$s %1$s, %2$s och %3$d mer - max antal flikar %1$d uppnådd + + max antal flikar %1$d uppnådd + Media: %s Innehållsvarning: %s diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 5d418817..5c562a07 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -171,7 +171,9 @@ %1$s, %2$s, %3$s மற்றும் %4$d மற்றவர்கள் %1$s, %2$s, மற்றும் %3$s %1$s மற்றும் %2$s - %d புதிய ஊடாடுதல் + + %d புதிய ஊடாடுதல் + கணக்கு மூடப்பட்டது பற்றி Tusky(டஸ்கி) %s diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index a775defa..cb11f26c 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -71,7 +71,9 @@ ไม่มีคำอธิบาย เตือนเนื้อหา : %s สื่อ: %s - ถึงจำนวนแท็บสูงสุดคือ %1$d แล้ว + + ถึงจำนวนแท็บสูงสุดคือ %1$d แล้ว + %1$s, %2$s และอีก %3$d %1$s และ %2$s %1$s @@ -128,8 +130,10 @@ ลบ ตั้งคำอธิบายล้มเหลว ตั้งคำอธิบาย - อธิบายเพื่อผู้บกพร่องทางสายตา -\n(จำกัด %d ตัวอักขระ) + + อธิบายเพื่อผู้บกพร่องทางสายตา +\n(จำกัด %d ตัวอักขระ) + โพสต์ด้วยบัญชี %1$s ลบบัญชีออกจากรายการ เพิ่มบัญชีไปใส่รายการ @@ -183,7 +187,9 @@ ขับเคลื่อนด้วย Tusky Tusky %s บัญชีไม่สาธารณะ - การโต้ตอบใหม่จำนวน %d + + การโต้ตอบใหม่จำนวน %d + %1$s และ %2$s %1$s, %2$s, และ %3$s %1$s, %2$s, %3$s และอีก %4$d คน @@ -478,7 +484,9 @@ ล้มเหลวในการโหลดข้อมูลตอบกลับ ฉบับร่างเก่า คุณต้องการลบลิสต์ %s ใช่ไหม\? - คุณไม่สามารถอัปโหลดไฟล์แนบมากกว่า %1$d ได้ + + คุณไม่สามารถอัปโหลดไฟล์แนบมากกว่า %1$d ได้ + บันทึกแล้ว! ไม่มีประกาศ ไม่มีกำหนด diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 6e3a3e0c..4320b973 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -201,7 +201,9 @@ %1$s, %2$s, %3$s ve %4$d diğer %1$s, %2$s ve %3$s %1$s ve %2$s - %d yeni etkileşim + + %d yeni etkileşim + Kitli Hesap Hakkında Tusky %s @@ -243,8 +245,10 @@ Listeler Zaman çizelgesini listele %1$s hesabıyla gönderiliyor - Görsel engelli için tanımla -\n(%d karakter limiti) + + Görsel engelli için tanımla +\n(%d karakter limiti) + Başlık belirle Kaldır Hesabı Kilitle @@ -299,7 +303,9 @@ %1$s %1$s ve %2$s %1$s, %2$s ve %3$d daha fazlası - %1$d maksimum sekme sayısına ulaşıldı + + %1$d maksimum sekme sayısına ulaşıldı + Gizli alanadları Boostu kaldır Favoriyi kaldır diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 5c49c828..6144773a 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -1,6 +1,6 @@ - Сталася неочікувана помилка. + Сталася помилка. \@%s Ліцензії Редагувати профіль @@ -10,36 +10,36 @@ Підписки Прикріплені З відповідями - Глобальна - Локальна + Загальні + Локальні Приватні повідомлення Сповіщення Головна - Помилка при надісланні поста. + Помилка надіслання допису. Зображення та відео не можуть бути прикріплені до статусу одночасно. Потрібен дозвіл на зберігання медіа. Потрібен дозвіл на читання медіа. - Файл не вдається відкрити. - Файл такого типу неможливо завантажити. + Не вдається відкрити цей файл. + Неможливо відвантажити файл цього типу. Аудіофайли повинні бути менше 40 МБ. Відео повинне бути менше 40 МБ. Файл повинен бути менше 8 МБ. - Статус занадто довгий! - Не вдалося знайти веб-браузер, який можна використати. + Статус надто довгий! + Не вдалося знайти браузер, який можна використати. Не може бути порожнім. - Сталася помилка мережі! Будь ласка, перевірте інтернет-з\'єднання і спробуйте знову! + Сталася помилка мережі! Перевірте інтернет-з\'єднання та спробуйте знову! Списки Списки - Про додаток + Про застосунок Скинути Пошук Редагувати профіль - Налаштування акаунта + Налаштування облікового запису Налаштування Вийти Чернетки Вподобане - Увійти + Увійти з Mastodon Зʼєднання… Немає результатів Пошук… @@ -56,10 +56,10 @@ Показати, хто вподобав Згадки Посилання - Попередження про контент - Заплановані пости + Попередження про вміст + Заплановані дмухи Чернетки - Відмовити + Відхилити Прийняти Скасувати Змінити @@ -92,25 +92,25 @@ Написати Не подобається Додати в закладки - Подобається + Вподобати Відповісти Швидка відповідь Додаткові коментарі\? Поскаржитися на @%s - %s відправив(-ла) запит на підписку - %s підписався(-лась) на вас + %s надсилає запит на підписку + %s підписується на вас Тут нічого немає. Потягніть вниз, щоб оновити! Тут нічого немає. Згорнути Розгорнути Натисніть для перегляду Медіа приховано - Попередження про контент + Попередження про вміст Змінити Написати - Скасувати приглушення бесіди - Приглушити бесіду - Заплановані пости + Скасувати приглушення розмови + Заглушити розмову + Заплановані дмухи Підписники Написати Медіа @@ -119,32 +119,418 @@ Заблоковані користувачі Вподобане Запити на підписку - Розблокувати %s - Відмінити приглушення + Не глушити %s + Не глушити Приховані домени - Список глушіння - ТООТ! - ТООТ + Заглушені користувачі + ДМУХНУТИ! + ДМУХНУТИ Показати просування Приховати просування Розгорнути - Забртаи просунення + Прибрати просування Просунути - %s сподабався ваш статус - %s просунув(ла) ваш статус + %s вподобує ваш дмух + %s просуває ваш дмух Згорнути Розгорнути - Чутливий вміст - %s Просунув(ла) + Делікатний вміст + %s просуває Приховані домени Заглушені користувачі Дописи - Поширити + Дмухнути Вкладки - Завантаження не вдалося. - Не вдалося отримати токін авторизації. - Авторизація була відхилина. - Сталася помилка неопізнаної авторизації. - Помилка входу з цією інстанцією. + Не вдалося відвантажити. + Не вдалося отримати токен входу. + Авторизацію відхилено. + Сталася помилка невпізнання авторизації. + Помилка автентифікації цього сервера. Введено недійсний домен + Показати просування + Показати просування + Вкладки + Не глушити %s + Видимість дмухів + Деякі відомості, які можуть вплинути на ваше психічний стан, буде приховано. Це включає: +\n +\n - Вподобання/Просування/Сповіщення про підписки +\n - Вподобання/Кількість просувань дмухів +\n - Статистика підписників/Публікацій у профілях +\n +\n На push-сповіщення це не вплине, але ви можете переглянути налаштування сповіщень вручну. + Вподобано + Вподобали + + %1$s вподобання + %1$s вподобання + %1$s вподобань + %1$s вподобань + + Сповіщати про вподобання кимось дмухів + мої дописи вподобано + Сховати медіа + Заглушити сповіщення від %s + Не глушити сповіщення від %s + %s щойно опубліковано + Оголошення + Відкрити меню + Емодзі Blob з Android 4.4–7.1 + Типовий набір емодзі пристрою + Виконання пошуку… + Спочатку потрібно буде завантажити ці набори емодзі + Типовий системний + Стиль емодзі + Скопійовано до буфера обміну + Ваш сервер %s не має власних емодзі + Зберегти чернетку\? + Вимагає затвердження підписників власноруч + Додати підпис + + Опис для людей з порушеннями зору + \n(до %d символів) + + Не вдалося додати підпис + Відписатися + Підписатися + Збережено! + Вибір %d + Кілька виборів + Додати вибір + 7 днів + 3 дні + 1 день + 6 годин + 1 година + 30 хвилин + 5 хвилин + Безкінечно + Тривалість + Опитування + Облікові записи + Не вдалося звітувати + Додаткові коментарі + Звіт @%s надіслано + Готово + Назад + Продовжити + + Залишилася %d секунда + Залишилося %d секунди + Залишилося %d секунд + Залишилося %d секунд + + + Залишилася %d хвилина + Залишилося %d хвилини + Залишилося %d хвилин + Залишилося %d хвилин + + + Залишилася %d година + Залишилося %d години + Залишилося %d годин + Залишилося %d годин + + + Залишився %d день + Залишилося %d дні + Залишилося %d днів + Залишилося %d днів + + Створене вами опитування завершилося + Опитування, в якому ви проголосували + Голосувати + закрито + завершується о %s + + %s особа + %s особи + %s осіб + %s осіб + + + %s голос + %s голоси + %s голосів + %s голосів + + %1$s • %2$s + Без опису + Попередження про вміст: %s + Медіа: %s + Просунули + Вміст + CC-BY-SA 4.0 + CC-BY 4.0 + Бот + Перезапустити + Пізніше + Вам потрібно буде перезапустити Tusky, щоб застосувати ці зміни + Необхідно перезапустити застосунок + Відкрити дмух + Розгорнути/згорнути всі статуси + Копію дмуху збережено до ваших чернеток + Надсилання скасовано + Надсилання дмухів + Помилка надсилання дмуху + Надсилання дмуху… + Оприлюднення з облікового запису %1$s + Вилучити обліковий запис зі списку + Додати обліковий запис до списку + Пошук серед тих, на кого ви підписані + Змінити список + Видалити список + Змінити назву списку + Створити список + Не вдалося видалити список + Не вдалося перейменувати список + Не вдалося створити список + Стрічка списку + Додати новий обліковий запис Mastodon + Додати обліковий запис + Фільтрувати фразу + Коли ключове слово або фраза є лише буквено-цифровими, вони застосовуватимуться лише, якщо вони збігатимуться з цілим словом + Ціле слово + Оновити + Заблокувати обліковий запис + Вилучити + Вилучити + Редагувати фільтр + Додати фільтр + Розмови + Загальнодоступні стрічки + завантажити ще + Відповідь для @%s + Завжди розгортати дмухи, з попередженнями про вміст + Підписники + Завжди показувати делікатний вміст + %dс + %dхв + %dгод + %dдн + %dр. + за %dс + за %dхв + за %dгод + за %dдн + за %dр. + Запит на підписку надіслано + Вкладення + Звуки + Відео + Зображення + Поділитися посиланням на дмух + Поділитися вмістом дмуху + Профіль Tusky + Звіти про вади та запити функцій: +\n https://github.com/tuskyapp/Tusky/issues + Вебсайт проєкту: +\n https://tusky.app + Tusky — вільне та відкрите програмне забезпечення. Ліцензовано загальною громадською ліцензією GNU версії 3, ви можете переглянути ліцензію тут: https://www.gnu.org/licenses/gpl-3.0.en.html + Створено Tusky + Tusky %s + Заблокований обліковий запис + + %d нова взаємодія + %d нові взаємодії + %d нових взаємодій + %d нових взаємодій + + %1$s, %2$s та ще %3$d + %1$s та %2$s + %1$s + %1$s та %2$s + %1$s, %2$s, та %3$s + %1$s, %2$s, %3$s та %4$d інших + %s згадує вас + Сповіщати про нові дмухи осіб, на яких ви підписалися + Нові дмухи + Сповіщати про завершення опитувань + Опитування + Сповіщати про просування кимось + Просування + Сповіщати про нові запити на підписки + Сповіщати про нових підписників + Нові підписники + Сповіщати про нові згадки + Нові згадки + Найбільший + Великий + Середній + Маленький + Найменший + Розмір шрифту статусу + Лише для підписників + Приховано + Приховано + Публічно + Публічно + Внизу + Вгорі + Розташування головної панелі переходів + Не вдалося синхронізувати налаштування + Публікування (синхронізовано з сервером) + Завжди позначати дописи делікатними + Типова приватність дописів + Порт HTTP-проксі + Сервер HTTP-проксі + Увімкнути HTTP-проксі + HTTP-проксі + Проксі + Завантаження попереднього перегляду медіа + Показати відповіді + Фільтрування стрічки + Анімувати власні емодзі + Показувати барвисті градієнти замість прихованих медіа + Анімовані GIF-аватарки + Показувати позначки для ботів + Мова + Ховати кнопку написати під час прокручування + Вкладки вбудованого браузера Chrome + Браузер + Тема системи + Автоматична від заходу сонця + Чорна + Світла + Темна + Фільтри + Стрічки + Тема застосунку + Вигляд + хтось, на кого мене підписано, публікує новий дмух + опитування завершено + мої дописи просунуто + отримано запит на підписку + хтось підписується + мене згадано + Сповіщати мене коли + Світлосповіщення + Вібросповіщення + Звукові сповіщення + Попередження + Безпосередньо: Опублікувати лише для згаданих користувачів + Лише підписники: Опублікувати лише для підписників + Приховано: Не показувати у загальних стрічках + Публічно: Опублікувати у загальних стрічках + Сховати сповіщення + Заглушити @%s\? + Заблокувати @%s\? + Сховати весь домен + Ви впевнені, що хочете заблокувати все з %s\? Ви не побачите вміст із цього домену в жодних загальнодоступних стрічках або у своїх сповіщеннях. Ваших підписників з цього домену буде видалено. + Видалити й переписати цей дмух\? + Видалити цей дмух\? + Не стежити за цим обліковим записом\? + Відкликати запит на підписку\? + Завантаження + Відвантаження… + Завершення відвантаження медіа + Сюди можна ввести адресу або домен будь-якого сервера, наприклад mastodon.social, icosahedron.website, social.tchncs.de та більше! +\n +\nЯкщо у вас ще немає облікового запису, ви можете ввести назву сервера, до якого ви хочете приєднатися та створити там обліковий запис. +\n +\nСервер — єдине місце, де розміщено ваш обліковий запис, але ви можете легко спілкуватися з людьми та стежити за ними на інших серверах, ніби ви перебуваєте на тому ж сайті. +\n +\nДокладніше на joinmastodon.org. + Що таке сервер\? + Котрий сервер\? + Заголовок + Аватар + Відповісти… + Показуване ім\'я + Відповідь успішно надіслано. + %s показано + Глушіння користувача прибрано + Користувача розблоковано + Поділитися медіа з… + Поділитися дмухом з… + Поділитися URL-адресою дмуха з… + Завантаження медіа + Завантажити медіа + Відкрити медіа #%d + Хештеги + Хештеги + Хештеги + Відкрити автора просування + Додати вкладку + Запланувати дмух + Клавіотура емодзі + Дмух, для якого ви створили чернетку відповіді, вилучено + Чернетку видалено + Не вдалося завантажити дані відповіді + Старі чернетки + Функція чернетки в Tusky була повністю перероблена, щоб бути швидшою, зручнішою для користувачів і з меншою кількістю вад. +\n Ви все ще можете отримати доступ до своїх старих чернеток за допомогою кнопки на екрані нових чернеток, але вони будуть вилучені в майбутньому оновленні! + Не вдалося надіслати цей дмух! + Ви дійсно хочете видалити список %s\? + + Ви не можете завантажити більше ніж %1$d медіавкладення. + Ви не можете завантажити більше ніж %1$d медіавкладення. + Ви не можете завантажити більше ніж %1$d медіавкладень. + Ви не можете завантажити більше ніж %1$d медіавкладень. + + Приховати кількісну статистику профілів + Приховати кількісну статистику дописів + Обмеження сповіщень стрічки + Переглянути сповіщення + Ваша особиста примітка щодо цього облікового запису + Добробут + Сховати заголовок верхньої панелі інструментів + Запитувати підтвердження перед просуванням + Показувати попередній перегляд посилань у стрічках + Найкоротший час планування Mastodon становить 5 хвилин. + Оголошень немає. + Черга статусів порожня. + У вас немає чернеток. + Помилка пошуку допису %s + Увімкнути перемикання між вкладками жестом проведення пальцем + Показати фільтр сповіщень + Не вдалося здійснити пошук + Обліковий запис з іншого сервера. Надіслати анонімізовану копію звіту й туди\? + Скаргу буде надіслано вашому модератору сервера. Ви можете надати пояснення, чому ви повідомляєте про цей обліковий запис знизу: + Не вдалося отримати статуси + Переслати до %s + Дії для зображення %s + Ви впевнені, що хочете остаточно очистити всі сповіщення\? + Створити дмух + Застосувати + Фільтр + Очистити + Список + Вибрати список + Хештег без # + Додати хештег + Назва списку + Опитування з варіантами: %1$s, %2$s, %3$s, %4$s; %5$s + Безпосередньо + Додано до закладок + Просунуто + + досягнено обмеження %1$d вкладка + досягнено обмеження %1$d вкладки + досягнено обмеження %1$d вкладок + досягнено обмеження %1$d вкладок + + + %s просування + %s просування + %s просувань + %s просувань + + Прикріпити + Відкріпити + Наведені далі відомості можуть відбивати не повний профіль користувача. Натисніть, щоб відкрити повний профіль у браузері. + Показ абсолютного часу + Ярлик + додати дані + Метадані профілю + Ліцензовано ліцензією Apache (копія знизу) + Tusky містить код та засоби з таких проєктів з відкритим кодом: + Відкликати просування + Просунути початковій аудиторії + %1$s переміщено до: + Не вдалося завантажити + Поточний набір емодзі Google + Стандартний набір емодзі Mastodon + Навіть попри те, що ваш обліковий запис загальнодоступний, співробітники %1$s вважають, що ви, можливо, захочете переглянути запити від цих облікових записів власноруч. \ No newline at end of file diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 9cebe1fc..5c132817 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -5,7 +5,9 @@ Hủy đăng Đăng Tút Đang đăng… - %d tương tác mới + + %d tương tác mới + %1$s và %2$s %1$s, %2$s, và %3$s %1$s, %2$s, %3$s và %4$d người khác @@ -66,13 +68,13 @@ Tải về Đang tải… Đã tải xong tập tin - Bạn phải nhập một tên miền, ví dụ mastodon.social, icosahedron.website, social.tchncs.de, và nhiều hơn nữa! + Bạn phải nhập một tên miền, ví dụ mastodon.social, icosahedron.website, social.tchncs.de, và vô số khác! \n \nNếu chưa có tài khoản, bạn phải tạo tài khoản trước ở đó. \n -\nMáy chủ, nói cách khác là một cộng đồng nơi mà tài khoản của bạn lưu trữ trên đó, nhưng bạn vẫn có thể giao tiếp và theo dõi mọi người trên các máy chủ khác một cách dễ dàng. +\nMáy chủ, nói cách khác là một cộng đồng nơi mà tài khoản của bạn lưu trữ trên đó, nhưng bạn vẫn có thể dễ dàng giao tiếp và theo dõi mọi người trên các máy chủ khác. \n -\nTham khảo thêm tại joinmastodon.org. +\nTham khảo joinmastodon.org. Đang kết nối… Ảnh bìa Ảnh đại diện @@ -134,13 +136,13 @@ Tạo bình chọn Thêm tệp Mở trong trình duyệt - Album + Media Yêu cầu theo dõi Máy chủ đã ẩn Người dùng đã chặn Người dùng đã ẩn - Đã lưu - Lượt thích + Lưu + Thích Trang cá nhân Đóng Thử lại @@ -184,24 +186,24 @@ Máy chủ đã ẩn Người dùng đã chặn Người dùng đã ẩn - Đã lưu + Những tút đã lưu Người theo dõi Theo dõi Ghim - Rép + Trả lời Tút Tút Xếp tab Tin nhắn Thế giới - Cộng đồng + Máy chủ Thông báo Bảng tin Nháp - Lượt thích + Những tút đã thích Máy chủ là gì\? Tải xem trước hình ảnh - Hiện lượt trả lời + Hiện những trả lời Hiện lượt chia sẻ Tabs Lọc bảng tin @@ -235,7 +237,7 @@ Thông báo Thông báo Nhắn tin: Chỉ người được nhắc tới mới thấy - Người theo dõi: Ai đã theo dõi mới xem được + Người theo dõi: Ai đã theo dõi mới được xem Riêng tư: Không hiện trên bảng tin Công khai: Mọi người đều có thể thấy Ẩn @%s\? @@ -245,7 +247,7 @@ Yêu cầu theo dõi Thông báo về người theo dõi mới Người theo dõi mới - Thông báo về lược nhắc tới + Thông báo về lượt nhắc tới To nhất To Trung bình @@ -267,7 +269,7 @@ Bật proxy Dùng proxy Vượt tường lửa - Thông báo khi của bạn được chia sẻ + Thông báo khi tút của bạn được chia sẻ Chia sẻ Thông báo về lượt yêu cầu theo dõi Báo lỗi và đề xuất tính năng @@ -289,20 +291,20 @@ Cộng đồng xem thêm Trả lời @%s - Thư viện + Media Luôn hiện nội dung bị ẩn Luôn hiện nội dung nhạy cảm Đang theo dõi bạn %ds - %d phút - %d giờ - %d ngày - %d năm + %d phút trước + %d giờ trước + %d ngày trước + %d năm trước %ds %d phút - in %d giờ - in %d ngày - in %d năm + %d giờ + %d ngày + %d năm Yêu cầu theo dõi Video Hình ảnh @@ -338,29 +340,29 @@ Thêm ghi chú Đã gửi báo cáo @%s - %d giây nữa kết thúc + %d giây - %d phút nữa kết thúc + %d phút - %d giờ nữa kết thúc + %d giờ - %d ngày nữa kết thúc + %d ngày - Cuộc bình chọn bạn tạo đã kết thúc - Cuộc bình chọn của bạn đã kết thúc + Cuộc bình chọn của bạn đã kết thúc + Cuộc bình chọn đã kết thúc Bình chọn xong kết thúc lúc %s - %s người + %s người bình chọn - %s người + %s người bình chọn - %1$s • %2$s + %1$s • %2$s Mô tả cho hình %s Viết Viết tút @@ -383,7 +385,9 @@ Đã chia sẻ Không có mô tả Nội dung nhạy cảm: %s - tối đa %1$d tab + + tối đa %1$d tab + %1$s, %2$s và %3$d người nữa %1$s và %2$s %1$s @@ -398,14 +402,14 @@ Ghim Gỡ ghim Thông tin có thể hiển thị không đầy đủ. Nhấn để mở xem chi tiết trên trình duyệt. - Sử dụng thời gian của thiết bị + Sử dụng thời gian thiết bị Nội dung Nhãn thêm nội dung Metadata CC-BY-SA 4.0 CC-BY 4.0 - Licensed under the Apache License (sao chép bên dưới) + Giấy phép Apache (xem bên dưới) Tusky có sử dụng mã nguồn từ những dự án mã nguồn mở sau: Hủy chia sẻ Chia sẻ công khai @@ -433,8 +437,10 @@ Tài khoản riêng tư Hủy bỏ Mô tả - Mô tả dành cho người khiếm thị -\n(giới hạn %d chữ) + + Mô tả dành cho người khiếm thị +\n(giới hạn %d chữ) + Đăng bằng tài khoản %1$s Thêm tài khoản vào danh sách Xóa tài khoản khỏi danh sách @@ -456,11 +462,11 @@ Bỏ ẩn %s Ẩn tiêu đề tab Đã lưu! - Thêm ghi chú + Ghi chú Chưa có thông báo. Có gì mới\? Ẩn số liệu trên trang cá nhân - Ẩn tương tác trên tút + Ẩn số liệu trên tút Hạn chế thông báo trên bảng tin Chọn loại thông báo Các thông tin ảnh hưởng tới tâm lý hành vi của bạn sẽ bị ẩn. Bao gồm: @@ -470,12 +476,14 @@ \n - Số Người theo dõi/Tút trên trang cá nhân \n \nThông báo đẩy sẽ không ảnh hưởng, bạn có thể tự thiết lập trong phần cài đặt điện thoại của bạn. - Cai nghiện + Chống nghiện Thông báo khi người bạn đăng ký theo dõi đăng tút mới Tút mới người tôi đăng ký theo dõi đăng tút mới %s vừa đăng tút - Bạn không thể đính kèm quá %1$d tệp. + + Bạn không thể đính kèm quá %1$d tệp. + Vĩnh viễn Thời hạn Bạn thật sự muốn xóa danh sách %s\? @@ -491,4 +499,5 @@ Emoji động Ngưng nhận thông báo Nhận thông báo + Dù biết tài khoản của bạn công khai, quản trị viên %1$s vẫn nghĩ bạn hãy nên xem lại yêu cầu theo dõi từ những tài khoản lạ. \ No newline at end of file diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 0b688a9c..710997b4 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -223,7 +223,9 @@ %1$s,%2$s,%3$s 和 %4$d 等人 %1$s,%2$s 和 %3$s %1$s 和 %2$s - %d 个新互动 + + %d 个新互动 + 锁嘟用户 关于 Tusky Tusky %s @@ -288,8 +290,10 @@ 从列表中移除用户 以 %1$s 发布嘟文 设置图片标题失败 - 为视觉障碍用户提供的描述 -\n(限制 %d 字) + + 为视觉障碍用户提供的描述 +\n(限制 %d 字) + 设置图片标题 移除 保护你的帐户(锁嘟) @@ -345,7 +349,9 @@ %1$s %1$s 和 %2$s %1$s,%2$s 和 %3$d 等人 - 标签页不能超过 %1$d 个 + + 标签页不能超过 %1$d 个 + 媒体:%s 内容提醒:%s @@ -481,4 +487,42 @@ 公告 已保存 此账号的备注 + 取消关注 + 关注 + 该草稿回复的原嘟文已被删除 + 草稿已删除 + 加载回复信息失败 + 旧草稿 + Tusky 的草稿功能已被重新设计,现在它更快、更友好,Bug也更少。 +\n 旧草稿依然可以通过新草稿页面的按钮查看,但他们将在未来版本中移除! + 嘟文发送失败! + 确认删除列表 %s? + + 最多只可上传 %1$d 个媒体附件 + + 隐藏账号的统计信息 + 反馈通知 + 隐藏嘟文的统计信息 + 限制时间线通知 + 一些可能影响您精神状态的信息将被隐藏,这些信息包括: +\n +\n - 收藏、转发、关注通知 +\n - 收藏、转发数 +\n - 账号的已关注数量、嘟文数量 +\n +\n 推送通知不会被影响,但可以在通知设置中手动禁用。 + 健康模式 + 永久 + 持续时间 + + %s 人 + + 附件 + 音频 + 当有我关注的用户发送了新嘟文时 + 新嘟文 + 显示动态自定义Emoji + 关注的人发送了新嘟文 + %s 发送了新嘟文 + 即使您的账号未上锁,管理员 %1$s 认为您可能需要手动处理来自这些账号的关注请求。 \ No newline at end of file diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 96bffb66..4d16d4c8 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -223,7 +223,9 @@ %1$s, %2$s, %3$s 和 %4$d 人 %1$s, %2$s, 和 %3$s %1$s 和 %2$s - %d 個新互動 + + %d 個新互動 + 鎖嘟用戶 關於 Tusky Tusky 是基於 GNU General Public License Version 3 許可證開源的自由軟體完整的許可證協議:https://www.gnu.org/licenses/gpl-3.0.en.html @@ -287,7 +289,9 @@ 從列表中移除用戶 以 %1$s 發嘟文 設定圖片標題失敗 - 為視覺障礙用戶提供的描述\n(限制 %d 字) + + 為視覺障礙用戶提供的描述\n(限制 %d 字) + 設定圖片標題 移除 保護你的帳戶(鎖嘟) @@ -343,7 +347,9 @@ %1$s %1$s 和 %2$s %1$s, %2$s 和 %3$d 等人 - 標籤頁不能超過 %1$d 個 + + 標籤頁不能超過 %1$d 個 + 媒體: %s @@ -448,7 +454,9 @@ 舊的草稿 這條嘟文發送失敗! 你確定要刪除列表 %s? - 你無法上傳超過 %1$d 媒體附件。 + + 你無法上傳超過 %1$d 媒體附件。 + 已儲存! 你對此帳號的個人註記 隱藏頂端工具列的標題 @@ -485,8 +493,7 @@ 返回 繼續 - %s 人 - + %s 人 列表 選擇列表 diff --git a/app/src/main/res/values-zh-rMO/strings.xml b/app/src/main/res/values-zh-rMO/strings.xml index 636757b0..fda310d6 100644 --- a/app/src/main/res/values-zh-rMO/strings.xml +++ b/app/src/main/res/values-zh-rMO/strings.xml @@ -217,7 +217,9 @@ %1$s, %2$s, %3$s 和 %4$d 人 %1$s, %2$s, 和 %3$s %1$s 和 %2$s - %d 個新互動 + + %d 個新互動 + 鎖嘟用戶 關於 Tusky Tusky 是基於 GNU General Public License Version 3 許可證開源的自由軟體完整的許可證協議:https://www.gnu.org/licenses/gpl-3.0.en.html @@ -281,7 +283,9 @@ 從列表中移除用戶 以 %1$s 發嘟文 設定圖片標題失敗 - 為視覺障礙用戶提供的描述\n(限制 %d 字) + + 為視覺障礙用戶提供的描述\n(限制 %d 字) + 設定圖片標題 移除 保護你的帳戶(鎖嘟) @@ -337,7 +341,9 @@ %1$s %1$s 和 %2$s %1$s, %2$s 和 %3$d 等人 - 標籤頁不能超過 %1$d 個 + + 標籤頁不能超過 %1$d 個 + 媒體: %s diff --git a/app/src/main/res/values-zh-rSG/strings.xml b/app/src/main/res/values-zh-rSG/strings.xml index ab7a89f4..166a1dd0 100644 --- a/app/src/main/res/values-zh-rSG/strings.xml +++ b/app/src/main/res/values-zh-rSG/strings.xml @@ -220,7 +220,9 @@ %1$s, %2$s, %3$s 和 %4$d 人 %1$s, %2$s, 和 %3$s %1$s 和 %2$s - %d 个新互动 + + %d 个新互动 + 锁嘟用户 关于 Tusky Tusky %s @@ -285,7 +287,9 @@ 从列表中移除用户 以 %1$s 发布嘟文 设置图片标题失败 - 为视觉障碍用户提供的描述\n(限制 %d 字) + + 为视觉障碍用户提供的描述\n(限制 %d 字) + 设置图片标题 移除 保护你的帐户(锁嘟) @@ -341,7 +345,9 @@ %1$s %1$s 和 %2$s %1$s, %2$s 和 %3$d 等人 - 标签页不能超过 %1$d 个 + + 标签页不能超过 %1$d 个 + 媒体: %s diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 45c170a8..c041972c 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -223,7 +223,9 @@ %1$s, %2$s, %3$s 和 %4$d 人 %1$s, %2$s, 和 %3$s %1$s 和 %2$s - %d 個新互動 + + %d 個新互動 + 鎖嘟用戶 關於 Tusky Tusky 是基於 GNU General Public License Version 3 許可證開源的自由軟體完整的許可證協議:https://www.gnu.org/licenses/gpl-3.0.en.html @@ -287,7 +289,9 @@ 從列表中移除用戶 以 %1$s 發嘟文 設定圖片標題失敗 - 為視覺障礙用戶提供的描述\n(限制 %d 字) + + 為視覺障礙用戶提供的描述\n(限制 %d 字) + 設定圖片標題 移除 保護你的帳戶(鎖嘟) @@ -343,7 +347,9 @@ %1$s %1$s 和 %2$s %1$s, %2$s 和 %3$d 等人 - 標籤頁不能超過 %1$d 個 + + 標籤頁不能超過 %1$d 個 + 媒體: %s @@ -449,8 +455,7 @@ \n推播通知不會受到影響,但你可以手動檢查你的通知設定。 數位健康 - %s 人 - + %s 人 %s 剛剛發了新嘟文 %s 請求關注你 @@ -472,7 +477,9 @@ 5 分鐘 無限期 期間 - 你無法上傳超過 %1$d 媒體附件。 + + 你無法上傳超過 %1$d 媒體附件。 + 當你關注的人發布新嘟文時通知 新嘟文 我關注的人有新嘟文 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1325ba37..18211658 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -297,14 +297,17 @@ %1$s, %2$s, %3$s and %4$d others %1$s, %2$s, and %3$s %1$s and %2$s - %d new interactions + + %d new interaction + %d new interactions + Locked Account About Tusky %s Powered by Tusky - Tusky is free and open-source software. + Chinwag Social is free and open-source software. It is licensed under the GNU General Public License Version 3. You can view the license here: https://www.gnu.org/licenses/gpl-3.0.en.html