Merge tag 'v15.1' into develop

This commit is contained in:
Mike Barnes 2021-10-24 20:57:53 +11:00
commit 866958e010
258 changed files with 5874 additions and 3891 deletions

View file

@ -1,7 +1,7 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-parcelize'
apply from: "../instance-build.gradle" apply from: "../instance-build.gradle"
@ -20,8 +20,8 @@ android {
applicationId APP_ID applicationId APP_ID
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 29 targetSdkVersion 29
versionCode 80 versionCode 81
versionName "14.0-CW2" versionName "15.1-CW1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
@ -65,9 +65,6 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8
} }
androidExtensions {
experimental = true
}
buildFeatures { buildFeatures {
viewBinding true viewBinding true
} }
@ -101,7 +98,7 @@ project.tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
} }
ext.lifecycleVersion = "2.2.0" ext.lifecycleVersion = "2.2.0"
ext.roomVersion = '2.2.5' ext.roomVersion = '2.3.0'
ext.retrofitVersion = '2.9.0' ext.retrofitVersion = '2.9.0'
ext.okhttpVersion = '4.9.0' ext.okhttpVersion = '4.9.0'
ext.glideVersion = '4.11.0' ext.glideVersion = '4.11.0'
@ -117,7 +114,7 @@ dependencies {
implementation "androidx.fragment:fragment-ktx:1.2.5" implementation "androidx.fragment:fragment-ktx:1.2.5"
implementation "androidx.browser:browser:1.3.0" implementation "androidx.browser:browser:1.3.0"
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.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.exifinterface:exifinterface:1.3.2"
implementation "androidx.cardview:cardview:1.0.0" implementation "androidx.cardview:cardview:1.0.0"
implementation "androidx.preference:preference-ktx:1.1.1" implementation "androidx.preference:preference-ktx:1.1.1"
@ -126,6 +123,7 @@ dependencies {
implementation "androidx.emoji:emoji-appcompat:1.1.0" implementation "androidx.emoji:emoji-appcompat:1.1.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-livedata-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.lifecycle:lifecycle-reactivestreams-ktx:$lifecycleVersion"
implementation "androidx.constraintlayout:constraintlayout:2.0.4" implementation "androidx.constraintlayout:constraintlayout:2.0.4"
implementation "androidx.paging:paging-runtime-ktx:2.1.2" implementation "androidx.paging:paging-runtime-ktx:2.1.2"
@ -135,7 +133,7 @@ dependencies {
implementation "androidx.room:room-rxjava2:$roomVersion" implementation "androidx.room:room-rxjava2:$roomVersion"
kapt "androidx.room:room-compiler:$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:retrofit:$retrofitVersion"
implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion" implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion"
@ -143,7 +141,6 @@ dependencies {
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion" implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
implementation "com.squareup.okhttp3:logging-interceptor:$okhttpVersion" implementation "com.squareup.okhttp3:logging-interceptor:$okhttpVersion"
implementation "com.squareup.okhttp3:okhttp-tls:$okhttpVersion"
implementation "org.conscrypt:conscrypt-android:2.5.1" implementation "org.conscrypt:conscrypt-android:2.5.1"

View file

@ -9,19 +9,20 @@ import android.text.method.LinkMovementMethod
import android.text.style.URLSpan import android.text.style.URLSpan
import android.text.util.Linkify import android.text.util.Linkify
import android.widget.TextView import android.widget.TextView
import com.keylesspalace.tusky.databinding.ActivityAboutBinding
import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.util.CustomURLSpan import com.keylesspalace.tusky.util.CustomURLSpan
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import kotlinx.android.synthetic.main.activity_about.*
import kotlinx.android.synthetic.main.toolbar_basic.*
class AboutActivity : BottomSheetActivity(), Injectable { class AboutActivity : BottomSheetActivity(), Injectable {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_about)
setSupportActionBar(toolbar) val binding = ActivityAboutBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.includedToolbar.toolbar)
supportActionBar?.run { supportActionBar?.run {
setDisplayHomeAsUpEnabled(true) setDisplayHomeAsUpEnabled(true)
setDisplayShowHomeEnabled(true) setDisplayShowHomeEnabled(true)
@ -29,26 +30,24 @@ class AboutActivity : BottomSheetActivity(), Injectable {
setTitle(R.string.about_title_activity) 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()) { if(BuildConfig.CUSTOM_INSTANCE.isBlank()) {
aboutPoweredByTusky.hide() binding.aboutPoweredByTusky.hide()
} }
aboutLicenseInfoTextView.setClickableTextWithoutUnderlines(R.string.about_tusky_license) binding.aboutLicenseInfoTextView.setClickableTextWithoutUnderlines(R.string.about_tusky_license)
aboutWebsiteInfoTextView.setClickableTextWithoutUnderlines(R.string.about_project_site) binding.aboutWebsiteInfoTextView.setClickableTextWithoutUnderlines(R.string.about_project_site)
aboutBugsFeaturesInfoTextView.setClickableTextWithoutUnderlines(R.string.about_bug_feature_request_site) binding.aboutBugsFeaturesInfoTextView.setClickableTextWithoutUnderlines(R.string.about_bug_feature_request_site)
tuskyProfileButton.setOnClickListener { binding.tuskyProfileButton.setOnClickListener {
viewUrl(BuildConfig.SUPPORT_ACCOUNT_URL) viewUrl(BuildConfig.SUPPORT_ACCOUNT_URL)
} }
aboutLicensesButton.setOnClickListener { binding.aboutLicensesButton.setOnClickListener {
startActivityWithSlideInAnimation(Intent(this, LicenseActivity::class.java)) startActivityWithSlideInAnimation(Intent(this, LicenseActivity::class.java))
} }
} }
} }
private fun TextView.setClickableTextWithoutUnderlines(@StringRes textId: Int) { private fun TextView.setClickableTextWithoutUnderlines(@StringRes textId: Int) {
@ -73,5 +72,4 @@ private fun TextView.setClickableTextWithoutUnderlines(@StringRes textId: Int) {
setText(builder) setText(builder)
linksClickable = true linksClickable = true
movementMethod = LinkMovementMethod.getInstance() movementMethod = LinkMovementMethod.getInstance()
} }

View file

@ -50,6 +50,7 @@ import com.google.android.material.tabs.TabLayoutMediator
import com.keylesspalace.tusky.adapter.AccountFieldAdapter import com.keylesspalace.tusky.adapter.AccountFieldAdapter
import com.keylesspalace.tusky.components.compose.ComposeActivity import com.keylesspalace.tusky.components.compose.ComposeActivity
import com.keylesspalace.tusky.components.report.ReportActivity import com.keylesspalace.tusky.components.report.ReportActivity
import com.keylesspalace.tusky.databinding.ActivityAccountBinding
import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.entity.Relationship import com.keylesspalace.tusky.entity.Relationship
@ -63,8 +64,6 @@ import com.keylesspalace.tusky.view.showMuteAccountDialog
import com.keylesspalace.tusky.viewmodel.AccountViewModel import com.keylesspalace.tusky.viewmodel.AccountViewModel
import dagger.android.DispatchingAndroidInjector import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector import dagger.android.HasAndroidInjector
import kotlinx.android.synthetic.main.activity_account.*
import kotlinx.android.synthetic.main.view_account_moved.*
import java.text.NumberFormat import java.text.NumberFormat
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.abs import kotlin.math.abs
@ -78,6 +77,8 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
private val viewModel: AccountViewModel by viewModels { viewModelFactory } private val viewModel: AccountViewModel by viewModels { viewModelFactory }
private val binding: ActivityAccountBinding by viewBinding(ActivityAccountBinding::inflate)
private lateinit var accountFieldAdapter : AccountFieldAdapter private lateinit var accountFieldAdapter : AccountFieldAdapter
private var followState: FollowState = FollowState.NOT_FOLLOWING private var followState: FollowState = FollowState.NOT_FOLLOWING
@ -118,7 +119,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
loadResources() loadResources()
makeNotificationBarTransparent() makeNotificationBarTransparent()
setContentView(R.layout.activity_account) setContentView(binding.root)
// Obtain information to fill out the profile. // Obtain information to fill out the profile.
viewModel.setAccountInfo(intent.getStringExtra(KEY_ACCOUNT_ID)!!) viewModel.setAccountInfo(intent.getStringExtra(KEY_ACCOUNT_ID)!!)
@ -136,9 +137,9 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
if (viewModel.isSelf) { if (viewModel.isSelf) {
updateButtons() updateButtons()
saveNoteInfo.hide() binding.saveNoteInfo.hide()
} else { } else {
saveNoteInfo.visibility = View.INVISIBLE binding.saveNoteInfo.visibility = View.INVISIBLE
} }
} }
@ -158,16 +159,16 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
*/ */
private fun setupAccountViews() { private fun setupAccountViews() {
// Initialise the default UI states. // Initialise the default UI states.
accountFloatingActionButton.hide() binding.accountFloatingActionButton.hide()
accountFollowButton.hide() binding.accountFollowButton.hide()
accountMuteButton.hide() binding.accountMuteButton.hide()
accountFollowsYouTextView.hide() binding.accountFollowsYouTextView.hide()
// setup the RecyclerView for the account fields // setup the RecyclerView for the account fields
accountFieldAdapter = AccountFieldAdapter(this, animateEmojis) accountFieldAdapter = AccountFieldAdapter(this, animateEmojis)
accountFieldList.isNestedScrollingEnabled = false binding.accountFieldList.isNestedScrollingEnabled = false
accountFieldList.layoutManager = LinearLayoutManager(this) binding.accountFieldList.layoutManager = LinearLayoutManager(this)
accountFieldList.adapter = accountFieldAdapter binding.accountFieldList.adapter = accountFieldAdapter
val accountListClickListener = { v: View -> val accountListClickListener = { v: View ->
@ -179,15 +180,15 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
val accountListIntent = AccountListActivity.newIntent(this, type, viewModel.accountId) val accountListIntent = AccountListActivity.newIntent(this, type, viewModel.accountId)
startActivityWithSlideInAnimation(accountListIntent) startActivityWithSlideInAnimation(accountListIntent)
} }
accountFollowers.setOnClickListener(accountListClickListener) binding.accountFollowers.setOnClickListener(accountListClickListener)
accountFollowing.setOnClickListener(accountListClickListener) binding.accountFollowing.setOnClickListener(accountListClickListener)
accountStatuses.setOnClickListener { binding.accountStatuses.setOnClickListener {
// Make nice ripple effect on tab // Make nice ripple effect on tab
accountTabLayout.getTabAt(0)!!.select() binding.accountTabLayout.getTabAt(0)!!.select()
val poorTabView = (accountTabLayout.getChildAt(0) as ViewGroup).getChildAt(0) val poorTabView = (binding.accountTabLayout.getChildAt(0) as ViewGroup).getChildAt(0)
poorTabView.isPressed = true 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 // 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) val wellbeingEnabled = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_PROFILE, false)
if (wellbeingEnabled) { if (wellbeingEnabled) {
accountStatuses.hide() binding.accountStatuses.hide()
accountFollowers.hide() binding.accountFollowers.hide()
accountFollowing.hide() binding.accountFollowing.hide()
} }
} }
/** /**
@ -209,19 +209,19 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
// Setup the tabs and timeline pager. // Setup the tabs and timeline pager.
adapter = AccountPagerAdapter(this, viewModel.accountId) adapter = AccountPagerAdapter(this, viewModel.accountId)
accountFragmentViewPager.adapter = adapter binding.accountFragmentViewPager.adapter = adapter
accountFragmentViewPager.offscreenPageLimit = 2 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)) 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] tab.text = pageTitles[position]
}.attach() }.attach()
val pageMargin = resources.getDimensionPixelSize(R.dimen.tab_page_margin) 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?) { override fun onTabReselected(tab: TabLayout.Tab?) {
tab?.position?.let { position -> tab?.position?.let { position ->
(adapter.getFragment(position) as? ReselectableFragment)?.onReselect() (adapter.getFragment(position) as? ReselectableFragment)?.onReselect()
@ -237,17 +237,17 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
private fun setupToolbar() { private fun setupToolbar() {
// set toolbar top margin according to system window insets // set toolbar top margin according to system window insets
accountCoordinatorLayout.setOnApplyWindowInsetsListener { _, insets -> binding.accountCoordinatorLayout.setOnApplyWindowInsetsListener { _, insets ->
val top = insets.systemWindowInsetTop val top = insets.systemWindowInsetTop
val toolbarParams = accountToolbar.layoutParams as CollapsingToolbarLayout.LayoutParams val toolbarParams = binding.accountToolbar.layoutParams as CollapsingToolbarLayout.LayoutParams
toolbarParams.topMargin = top toolbarParams.topMargin = top
insets.consumeSystemWindowInsets() insets.consumeSystemWindowInsets()
} }
// Setup the toolbar. // Setup the toolbar.
setSupportActionBar(accountToolbar) setSupportActionBar(binding.accountToolbar)
supportActionBar?.run { supportActionBar?.run {
setDisplayHomeAsUpEnabled(true) setDisplayHomeAsUpEnabled(true)
setDisplayShowHomeEnabled(true) setDisplayShowHomeEnabled(true)
@ -258,9 +258,9 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
val toolbarBackground = MaterialShapeDrawable.createWithElevationOverlay(this, appBarElevation) val toolbarBackground = MaterialShapeDrawable.createWithElevationOverlay(this, appBarElevation)
toolbarBackground.fillColor = ColorStateList.valueOf(Color.TRANSPARENT) 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 { val avatarBackground = MaterialShapeDrawable.createWithElevationOverlay(this, appBarElevation).apply {
fillColor = ColorStateList.valueOf(toolbarColor) fillColor = ColorStateList.valueOf(toolbarColor)
@ -269,10 +269,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
.setAllCornerSizes(resources.getDimension(R.dimen.account_avatar_background_radius)) .setAllCornerSizes(resources.getDimension(R.dimen.account_avatar_background_radius))
.build() .build()
} }
accountAvatarImageView.background = avatarBackground binding.accountAvatarImageView.background = avatarBackground
// Add a listener to change the toolbar icon color when it enters/exits its collapsed state. // 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) { override fun onOffsetChanged(appBarLayout: AppBarLayout, verticalOffset: Int) {
@ -289,19 +289,19 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
if (hideFab && !viewModel.isSelf && !blocking) { if (hideFab && !viewModel.isSelf && !blocking) {
if (verticalOffset > oldOffset) { if (verticalOffset > oldOffset) {
accountFloatingActionButton.show() binding.accountFloatingActionButton.show()
} }
if (verticalOffset < oldOffset) { if (verticalOffset < oldOffset) {
accountFloatingActionButton.hide() binding.accountFloatingActionButton.hide()
} }
} }
val scaledAvatarSize = (avatarSize + verticalOffset) / avatarSize val scaledAvatarSize = (avatarSize + verticalOffset) / avatarSize
accountAvatarImageView.scaleX = scaledAvatarSize binding.accountAvatarImageView.scaleX = scaledAvatarSize
accountAvatarImageView.scaleY = scaledAvatarSize binding.accountAvatarImageView.scaleY = scaledAvatarSize
accountAvatarImageView.visible(scaledAvatarSize > 0) binding.accountAvatarImageView.visible(scaledAvatarSize > 0)
val transparencyPercent = (abs(verticalOffset) / titleVisibleHeight.toFloat()).coerceAtMost(1f) val transparencyPercent = (abs(verticalOffset) / titleVisibleHeight.toFloat()).coerceAtMost(1f)
@ -311,7 +311,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
toolbarBackground.fillColor = ColorStateList.valueOf(evaluatedToolbarColor) 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) { when (it) {
is Success -> onAccountChanged(it.data) is Success -> onAccountChanged(it.data)
is Error -> { 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() } .setAction(R.string.action_retry) { viewModel.refresh() }
.show() .show()
} }
@ -344,7 +344,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
} }
if (it is Error) { 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() } .setAction(R.string.action_retry) { viewModel.refresh() }
.show() .show()
} }
@ -355,7 +355,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
accountFieldAdapter.notifyDataSetChanged() accountFieldAdapter.notifyDataSetChanged()
}) })
viewModel.noteSaved.observe(this) { 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 * Setup swipe to refresh layout
*/ */
private fun setupRefreshLayout() { private fun setupRefreshLayout() {
swipeToRefreshLayout.setOnRefreshListener { binding.swipeToRefreshLayout.setOnRefreshListener {
viewModel.refresh() viewModel.refresh()
adapter.refreshContent() adapter.refreshContent()
} }
viewModel.isRefreshing.observe(this, { isRefreshing -> 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?) { private fun onAccountChanged(account: Account?) {
loadedAccount = account ?: return loadedAccount = account ?: return
val usernameFormatted = getString(R.string.status_username_format, account.username) val usernameFormatted = getString(R.string.status_username_format, account.username)
accountUsernameTextView.text = usernameFormatted binding.accountUsernameTextView.text = usernameFormatted
accountDisplayNameTextView.text = account.name.emojify(account.emojis, accountDisplayNameTextView, animateEmojis) binding.accountDisplayNameTextView.text = account.name.emojify(account.emojis, binding.accountDisplayNameTextView, animateEmojis)
val emojifiedNote = account.note.emojify(account.emojis, accountNoteTextView, animateEmojis) val emojifiedNote = account.note.emojify(account.emojis, binding.accountNoteTextView, animateEmojis)
LinkHelper.setClickableText(accountNoteTextView, emojifiedNote, null, this) LinkHelper.setClickableText(binding.accountNoteTextView, emojifiedNote, null, this)
// accountFieldAdapter.fields = account.fields ?: emptyList() // accountFieldAdapter.fields = account.fields ?: emptyList()
accountFieldAdapter.emojis = account.emojis ?: emptyList() accountFieldAdapter.emojis = account.emojis ?: emptyList()
accountFieldAdapter.notifyDataSetChanged() accountFieldAdapter.notifyDataSetChanged()
accountLockedImageView.visible(account.locked) binding.accountLockedImageView.visible(account.locked)
accountBadgeTextView.visible(account.bot) binding.accountBadgeTextView.visible(account.bot)
updateAccountAvatar() updateAccountAvatar()
updateToolbar() updateToolbar()
@ -397,7 +397,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
updateAccountStats() updateAccountStats()
invalidateOptionsMenu() invalidateOptionsMenu()
accountMuteButton.setOnClickListener { binding.accountMuteButton.setOnClickListener {
viewModel.unmuteAccount() viewModel.unmuteAccount()
updateMuteButton() updateMuteButton()
} }
@ -411,7 +411,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
loadAvatar( loadAvatar(
account.avatar, account.avatar,
accountAvatarImageView, binding.accountAvatarImageView,
resources.getDimensionPixelSize(R.dimen.avatar_radius_94dp), resources.getDimensionPixelSize(R.dimen.avatar_radius_94dp),
animateAvatar animateAvatar
) )
@ -420,10 +420,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
.asBitmap() .asBitmap()
.load(account.header) .load(account.header)
.centerCrop() .centerCrop()
.into(accountHeaderImageView) .into(binding.accountHeaderImageView)
accountAvatarImageView.setOnClickListener { avatarView -> binding.accountAvatarImageView.setOnClickListener { avatarView ->
val intent = ViewMediaActivity.newSingleImageIntent(avatarView.context, account.avatar) val intent = ViewMediaActivity.newSingleImageIntent(avatarView.context, account.avatar)
avatarView.transitionName = account.avatar avatarView.transitionName = account.avatar
@ -440,7 +440,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
private fun updateToolbar() { private fun updateToolbar() {
loadedAccount?.let { account -> loadedAccount?.let { account ->
val emojifiedName = account.name.emojify(account.emojis, accountToolbar, animateEmojis) val emojifiedName = account.name.emojify(account.emojis, binding.accountToolbar, animateEmojis)
try { try {
supportActionBar?.title = EmojiCompat.get().process(emojifiedName) supportActionBar?.title = EmojiCompat.get().process(emojifiedName)
@ -457,28 +457,27 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
private fun updateMovedAccount() { private fun updateMovedAccount() {
loadedAccount?.moved?.let { movedAccount -> loadedAccount?.moved?.let { movedAccount ->
accountMovedView?.show() binding.accountMovedView.show()
// necessary because accountMovedView is now replaced in layout hierachy binding.accountMovedView.setOnClickListener {
findViewById<View>(R.id.accountMovedViewLayout).setOnClickListener {
onViewAccount(movedAccount.id) onViewAccount(movedAccount.id)
} }
accountMovedDisplayName.text = movedAccount.name binding.accountMovedDisplayName.text = movedAccount.name
accountMovedUsername.text = getString(R.string.status_username_format, movedAccount.username) binding.accountMovedUsername.text = getString(R.string.status_username_format, movedAccount.username)
val avatarRadius = resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp) 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 // this is necessary because API 19 can't handle vector compound drawables
val movedIcon = ContextCompat.getDrawable(this, R.drawable.ic_briefcase)?.mutate() val movedIcon = ContextCompat.getDrawable(this, R.drawable.ic_briefcase)?.mutate()
val textColor = ThemeUtils.getColor(this, android.R.attr.textColorTertiary) val textColor = ThemeUtils.getColor(this, android.R.attr.textColorTertiary)
movedIcon?.colorFilter = PorterDuffColorFilter(textColor, PorterDuff.Mode.SRC_IN) 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() { private fun updateRemoteAccount() {
loadedAccount?.let { account -> loadedAccount?.let { account ->
if (account.isRemote()) { if (account.isRemote()) {
accountRemoveView.show() binding.accountRemoveView.show()
accountRemoveView.setOnClickListener { binding.accountRemoveView.setOnClickListener {
LinkHelper.openLink(account.url, this) LinkHelper.openLink(account.url, this)
} }
} }
@ -503,13 +502,13 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
private fun updateAccountStats() { private fun updateAccountStats() {
loadedAccount?.let { account -> loadedAccount?.let { account ->
val numberFormat = NumberFormat.getNumberInstance() val numberFormat = NumberFormat.getNumberInstance()
accountFollowersTextView.text = numberFormat.format(account.followersCount) binding.accountFollowersTextView.text = numberFormat.format(account.followersCount)
accountFollowingTextView.text = numberFormat.format(account.followingCount) binding.accountFollowingTextView.text = numberFormat.format(account.followingCount)
accountStatusesTextView.text = numberFormat.format(account.statusesCount) binding.accountStatusesTextView.text = numberFormat.format(account.statusesCount)
accountFloatingActionButton.setOnClickListener { mention() } binding.accountFloatingActionButton.setOnClickListener { mention() }
accountFollowButton.setOnClickListener { binding.accountFollowButton.setOnClickListener {
if (viewModel.isSelf) { if (viewModel.isSelf) {
val intent = Intent(this@AccountActivity, EditProfileActivity::class.java) val intent = Intent(this@AccountActivity, EditProfileActivity::class.java)
startActivity(intent) startActivity(intent)
@ -552,14 +551,14 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
val preferences = PreferenceManager.getDefaultSharedPreferences(this) val preferences = PreferenceManager.getDefaultSharedPreferences(this)
val wellbeingEnabled = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_PROFILE, false) 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 // 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 // it's also now supported in Mastodon 3.3.0rc but called notifying and use different API call
if(!viewModel.isSelf && followState == FollowState.FOLLOWING if(!viewModel.isSelf && followState == FollowState.FOLLOWING
&& (relation.subscribing != null || relation.notifying != null)) { && (relation.subscribing != null || relation.notifying != null)) {
accountSubscribeButton.show() binding.accountSubscribeButton.show()
accountSubscribeButton.setOnClickListener { binding.accountSubscribeButton.setOnClickListener {
viewModel.changeSubscribingState() viewModel.changeSubscribingState()
} }
if(relation.notifying != null) 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 // 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) binding.accountNoteTextInputLayout.visible(relation.note != null)
accountNoteTextInputLayout.editText?.setText(relation.note) binding.accountNoteTextInputLayout.editText?.setText(relation.note)
accountNoteTextInputLayout.editText?.addTextChangedListener(noteWatcher) binding.accountNoteTextInputLayout.editText?.addTextChangedListener(noteWatcher)
updateButtons() updateButtons()
} }
@ -587,22 +586,22 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
private fun updateFollowButton() { private fun updateFollowButton() {
if (viewModel.isSelf) { if (viewModel.isSelf) {
accountFollowButton.setText(R.string.action_edit_own_profile) binding.accountFollowButton.setText(R.string.action_edit_own_profile)
return return
} }
if (blocking) { if (blocking) {
accountFollowButton.setText(R.string.action_unblock) binding.accountFollowButton.setText(R.string.action_unblock)
return return
} }
when (followState) { when (followState) {
FollowState.NOT_FOLLOWING -> { FollowState.NOT_FOLLOWING -> {
accountFollowButton.setText(R.string.action_follow) binding.accountFollowButton.setText(R.string.action_follow)
} }
FollowState.REQUESTED -> { FollowState.REQUESTED -> {
accountFollowButton.setText(R.string.state_follow_requested) binding.accountFollowButton.setText(R.string.state_follow_requested)
} }
FollowState.FOLLOWING -> { FollowState.FOLLOWING -> {
accountFollowButton.setText(R.string.action_unfollow) binding.accountFollowButton.setText(R.string.action_unfollow)
} }
} }
updateSubscribeButton() updateSubscribeButton()
@ -610,23 +609,23 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
private fun updateMuteButton() { private fun updateMuteButton() {
if (muting) { if (muting) {
accountMuteButton.setIconResource(R.drawable.ic_unmute_24dp) binding.accountMuteButton.setIconResource(R.drawable.ic_unmute_24dp)
} else { } else {
accountMuteButton.hide() binding.accountMuteButton.hide()
} }
} }
private fun updateSubscribeButton() { private fun updateSubscribeButton() {
if(followState != FollowState.FOLLOWING) { if(followState != FollowState.FOLLOWING) {
accountSubscribeButton.hide() binding.accountSubscribeButton.hide()
} }
if(subscribing) { if(subscribing) {
accountSubscribeButton.setIconResource(R.drawable.ic_notifications_active_24dp) binding.accountSubscribeButton.setIconResource(R.drawable.ic_notifications_active_24dp)
accountSubscribeButton.contentDescription = getString(R.string.action_unsubscribe_account) binding.accountSubscribeButton.contentDescription = getString(R.string.action_unsubscribe_account)
} else { } else {
accountSubscribeButton.setIconResource(R.drawable.ic_notifications_24dp) binding.accountSubscribeButton.setIconResource(R.drawable.ic_notifications_24dp)
accountSubscribeButton.contentDescription = getString(R.string.action_subscribe_account) binding.accountSubscribeButton.contentDescription = getString(R.string.action_subscribe_account)
} }
} }
@ -635,27 +634,27 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
if (loadedAccount?.moved == null) { if (loadedAccount?.moved == null) {
accountFollowButton.show() binding.accountFollowButton.show()
updateFollowButton() updateFollowButton()
if (blocking || viewModel.isSelf) { if (blocking || viewModel.isSelf) {
accountFloatingActionButton.hide() binding.accountFloatingActionButton.hide()
accountMuteButton.hide() binding.accountMuteButton.hide()
accountSubscribeButton.hide() binding.accountSubscribeButton.hide()
} else { } else {
accountFloatingActionButton.show() binding.accountFloatingActionButton.show()
if (muting) if (muting)
accountMuteButton.show() binding.accountMuteButton.show()
else else
accountMuteButton.hide() binding.accountMuteButton.hide()
updateMuteButton() updateMuteButton()
} }
} else { } else {
accountFloatingActionButton.hide() binding.accountFloatingActionButton.hide()
accountFollowButton.hide() binding.accountFollowButton.hide()
accountMuteButton.hide() binding.accountMuteButton.hide()
accountSubscribeButton.hide() binding.accountSubscribeButton.hide()
} }
} }
@ -833,7 +832,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
override fun getActionButton(): FloatingActionButton? { override fun getActionButton(): FloatingActionButton? {
return if (!viewModel.isSelf && !blocking) { return if (!viewModel.isSelf && !blocking) {
accountFloatingActionButton binding.accountFloatingActionButton
} else null } else null
} }

View file

@ -18,10 +18,10 @@ package com.keylesspalace.tusky
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import com.keylesspalace.tusky.databinding.ActivityAccountListBinding
import com.keylesspalace.tusky.fragment.AccountListFragment import com.keylesspalace.tusky.fragment.AccountListFragment
import dagger.android.DispatchingAndroidInjector import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector import dagger.android.HasAndroidInjector
import kotlinx.android.synthetic.main.toolbar_basic.*
import javax.inject.Inject import javax.inject.Inject
class AccountListActivity : BaseActivity(), HasAndroidInjector { class AccountListActivity : BaseActivity(), HasAndroidInjector {
@ -41,12 +41,14 @@ class AccountListActivity : BaseActivity(), HasAndroidInjector {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) 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 type = intent.getSerializableExtra(EXTRA_TYPE) as Type
val id: String? = intent.getStringExtra(EXTRA_ID) val id: String? = intent.getStringExtra(EXTRA_ID)
val accountLocked: Boolean = intent.getBooleanExtra(EXTRA_ACCOUNT_LOCKED, false)
setSupportActionBar(toolbar) setSupportActionBar(binding.includedToolbar.toolbar)
supportActionBar?.apply { supportActionBar?.apply {
when (type) { when (type) {
Type.BLOCKS -> setTitle(R.string.title_blocks) Type.BLOCKS -> setTitle(R.string.title_blocks)
@ -63,7 +65,7 @@ class AccountListActivity : BaseActivity(), HasAndroidInjector {
supportFragmentManager supportFragmentManager
.beginTransaction() .beginTransaction()
.replace(R.id.fragment_container, AccountListFragment.newInstance(type, id)) .replace(R.id.fragment_container, AccountListFragment.newInstance(type, id, accountLocked))
.commit() .commit()
} }
@ -72,12 +74,15 @@ class AccountListActivity : BaseActivity(), HasAndroidInjector {
companion object { companion object {
private const val EXTRA_TYPE = "type" private const val EXTRA_TYPE = "type"
private const val EXTRA_ID = "id" private const val EXTRA_ID = "id"
private const val EXTRA_ACCOUNT_LOCKED = "acc_locked"
@JvmStatic @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 { return Intent(context, AccountListActivity::class.java).apply {
putExtra(EXTRA_TYPE, type) putExtra(EXTRA_TYPE, type)
putExtra(EXTRA_ID, id) putExtra(EXTRA_ID, id)
putExtra(EXTRA_ACCOUNT_LOCKED, accountLocked)
} }
} }
} }

View file

@ -23,11 +23,13 @@ import android.view.ViewGroup
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.fragment.app.viewModels
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.ListAdapter 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.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.entity.Account 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.android.lifecycle.AndroidLifecycleScopeProvider.from
import com.uber.autodispose.autoDispose import com.uber.autodispose.autoDispose
import io.reactivex.android.schedulers.AndroidSchedulers 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 java.io.IOException
import javax.inject.Inject import javax.inject.Inject
@ -48,23 +47,11 @@ private typealias AccountInfo = Pair<Account, Boolean>
class AccountsInListFragment : DialogFragment(), Injectable { 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 @Inject
lateinit var viewModelFactory: ViewModelFactory 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 listId: String
private lateinit var listName: String private lateinit var listName: String
@ -79,7 +66,6 @@ class AccountsInListFragment : DialogFragment(), Injectable {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setStyle(STYLE_NORMAL, R.style.TuskyDialogFragmentStyle) setStyle(STYLE_NORMAL, R.style.TuskyDialogFragmentStyle)
viewModel = viewModelFactory.create(AccountsInListViewModel::class.java)
val args = requireArguments() val args = requireArguments()
listId = args.getString(LIST_ID_ARG)!! listId = args.getString(LIST_ID_ARG)!!
listName = args.getString(LIST_NAME_ARG)!! listName = args.getString(LIST_NAME_ARG)!!
@ -100,12 +86,11 @@ class AccountsInListFragment : DialogFragment(), Injectable {
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) binding.accountsRecycler.layoutManager = LinearLayoutManager(view.context)
accountsRecycler.layoutManager = LinearLayoutManager(view.context) binding.accountsRecycler.adapter = adapter
accountsRecycler.adapter = adapter
accountsSearchRecycler.layoutManager = LinearLayoutManager(view.context) binding.accountsSearchRecycler.layoutManager = LinearLayoutManager(view.context)
accountsSearchRecycler.adapter = searchAdapter binding.accountsSearchRecycler.adapter = searchAdapter
viewModel.state viewModel.state
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
@ -114,15 +99,15 @@ class AccountsInListFragment : DialogFragment(), Injectable {
adapter.submitList(state.accounts.asRightOrNull() ?: listOf()) adapter.submitList(state.accounts.asRightOrNull() ?: listOf())
when (state.accounts) { when (state.accounts) {
is Either.Right -> messageView.hide() is Either.Right -> binding.messageView.hide()
is Either.Left -> handleError(state.accounts.value) is Either.Left -> handleError(state.accounts.value)
} }
setupSearchView(state) setupSearchView(state)
} }
searchView.isSubmitButtonEnabled = true binding.searchView.isSubmitButtonEnabled = true
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { binding.searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean { override fun onQueryTextSubmit(query: String?): Boolean {
viewModel.search(query ?: "") viewModel.search(query ?: "")
return true return true
@ -141,30 +126,30 @@ class AccountsInListFragment : DialogFragment(), Injectable {
private fun setupSearchView(state: State) { private fun setupSearchView(state: State) {
if (state.searchResult == null) { if (state.searchResult == null) {
searchAdapter.submitList(listOf()) searchAdapter.submitList(listOf())
accountsSearchRecycler.hide() binding.accountsSearchRecycler.hide()
accountsRecycler.show() binding.accountsRecycler.show()
} else { } else {
val listAccounts = state.accounts.asRightOrNull() ?: listOf() val listAccounts = state.accounts.asRightOrNull() ?: listOf()
val newList = state.searchResult.map { acc -> val newList = state.searchResult.map { acc ->
acc to listAccounts.contains(acc) acc to listAccounts.contains(acc)
} }
searchAdapter.submitList(newList) searchAdapter.submitList(newList)
accountsSearchRecycler.show() binding.accountsSearchRecycler.show()
accountsRecycler.hide() binding.accountsRecycler.hide()
} }
} }
private fun handleError(error: Throwable) { private fun handleError(error: Throwable) {
messageView.show() binding.messageView.show()
val retryAction = { _: View -> val retryAction = { _: View ->
messageView.hide() binding.messageView.hide()
viewModel.load(listId) viewModel.load(listId)
} }
if (error is IOException) { if (error is IOException) {
messageView.setup(R.drawable.elephant_offline, binding.messageView.setup(R.drawable.elephant_offline,
R.string.error_network, retryAction) R.string.error_network, retryAction)
} else { } else {
messageView.setup(R.drawable.elephant_error, binding.messageView.setup(R.drawable.elephant_error,
R.string.error_generic, retryAction) R.string.error_generic, retryAction)
} }
} }
@ -187,39 +172,28 @@ class AccountsInListFragment : DialogFragment(), Injectable {
} }
} }
inner class Adapter : ListAdapter<Account, Adapter.ViewHolder>(AccountDiffer) { inner class Adapter : ListAdapter<Account, BindingHolder<ItemFollowRequestBinding>>(AccountDiffer) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemFollowRequestBinding> {
val view = LayoutInflater.from(parent.context) val binding = ItemFollowRequestBinding.inflate(LayoutInflater.from(parent.context), parent, false)
.inflate(R.layout.item_follow_request, parent, false) val holder = BindingHolder(binding)
return ViewHolder(view)
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) { override fun onBindViewHolder(holder: BindingHolder<ItemFollowRequestBinding>, position: Int) {
holder.bind(getItem(position)) val account = getItem(position)
} holder.binding.displayNameTextView.text = account.name.emojify(account.emojis, holder.binding.displayNameTextView, animateEmojis)
holder.binding.usernameTextView.text = account.username
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), loadAvatar(account.avatar, holder.binding.avatar, radius, animateAvatar)
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)
}
} }
} }
@ -232,57 +206,58 @@ class AccountsInListFragment : DialogFragment(), Injectable {
return oldItem.second == newItem.second return oldItem.second == newItem.second
&& oldItem.first.deepEquals(newItem.first) && oldItem.first.deepEquals(newItem.first)
} }
} }
inner class SearchAdapter : ListAdapter<AccountInfo, SearchAdapter.ViewHolder>(SearchDiffer) { inner class SearchAdapter : ListAdapter<AccountInfo, BindingHolder<ItemFollowRequestBinding>>(SearchDiffer) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemFollowRequestBinding> {
val view = LayoutInflater.from(parent.context) val binding = ItemFollowRequestBinding.inflate(LayoutInflater.from(parent.context), parent, false)
.inflate(R.layout.item_follow_request, parent, false) val holder = BindingHolder(binding)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) { binding.notificationTextView.hide()
val (account, inAList) = getItem(position) binding.acceptButton.hide()
holder.bind(account, inAList) binding.rejectButton.setOnClickListener {
val (account, inAList) = getItem(holder.bindingAdapterPosition)
}
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)
if (inAList) { if (inAList) {
onRemoveFromList(account.id) onRemoveFromList(account.id)
} else { } else {
onAddToList(account) onAddToList(account)
} }
} }
return holder
}
override fun onBindViewHolder(holder: BindingHolder<ItemFollowRequestBinding>, 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 }
} }
} }
} }

View file

@ -38,6 +38,7 @@ import com.bumptech.glide.load.resource.bitmap.FitCenter
import com.bumptech.glide.load.resource.bitmap.RoundedCorners import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.adapter.AccountFieldEditAdapter import com.keylesspalace.tusky.adapter.AccountFieldEditAdapter
import com.keylesspalace.tusky.databinding.ActivityEditProfileBinding
import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.util.* 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.colorInt
import com.mikepenz.iconics.utils.sizeDp import com.mikepenz.iconics.utils.sizeDp
import com.theartofdev.edmodo.cropper.CropImage 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 import javax.inject.Inject
class EditProfileActivity : BaseActivity(), Injectable { class EditProfileActivity : BaseActivity(), Injectable {
@ -71,6 +70,8 @@ class EditProfileActivity : BaseActivity(), Injectable {
private val viewModel: EditProfileViewModel by viewModels { viewModelFactory } private val viewModel: EditProfileViewModel by viewModels { viewModelFactory }
private val binding by viewBinding(ActivityEditProfileBinding::inflate)
private var currentlyPicking: PickType = PickType.NOTHING private var currentlyPicking: PickType = PickType.NOTHING
private val accountFieldEditAdapter = AccountFieldEditAdapter() private val accountFieldEditAdapter = AccountFieldEditAdapter()
@ -88,33 +89,33 @@ class EditProfileActivity : BaseActivity(), Injectable {
currentlyPicking = PickType.valueOf(it) currentlyPicking = PickType.valueOf(it)
} }
setContentView(R.layout.activity_edit_profile) setContentView(binding.root)
setSupportActionBar(toolbar) setSupportActionBar(binding.includedToolbar.toolbar)
supportActionBar?.run { supportActionBar?.run {
setTitle(R.string.title_edit_profile) setTitle(R.string.title_edit_profile)
setDisplayHomeAsUpEnabled(true) setDisplayHomeAsUpEnabled(true)
setDisplayShowHomeEnabled(true) setDisplayShowHomeEnabled(true)
} }
avatarButton.setOnClickListener { onMediaPick(PickType.AVATAR) } binding.avatarButton.setOnClickListener { onMediaPick(PickType.AVATAR) }
headerButton.setOnClickListener { onMediaPick(PickType.HEADER) } binding.headerButton.setOnClickListener { onMediaPick(PickType.HEADER) }
fieldList.layoutManager = LinearLayoutManager(this) binding.fieldList.layoutManager = LinearLayoutManager(this)
fieldList.adapter = accountFieldEditAdapter binding.fieldList.adapter = accountFieldEditAdapter
val plusDrawable = IconicsDrawable(this, GoogleMaterial.Icon.gmd_add).apply { sizeDp = 12; colorInt = Color.WHITE } 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() accountFieldEditAdapter.addField()
if(accountFieldEditAdapter.itemCount >= MAX_ACCOUNT_FIELDS) { if(accountFieldEditAdapter.itemCount >= MAX_ACCOUNT_FIELDS) {
it.isVisible = false it.isVisible = false
} }
scrollView.post{ binding.scrollView.post{
scrollView.smoothScrollTo(0, it.bottom) binding.scrollView.smoothScrollTo(0, it.bottom)
} }
} }
@ -126,12 +127,12 @@ class EditProfileActivity : BaseActivity(), Injectable {
val me = profileRes.data val me = profileRes.data
if (me != null) { if (me != null) {
displayNameEditText.setText(me.displayName) binding.displayNameEditText.setText(me.displayName)
noteEditText.setText(me.source?.note) binding.noteEditText.setText(me.source?.note)
lockedCheckBox.isChecked = me.locked binding.lockedCheckBox.isChecked = me.locked
accountFieldEditAdapter.setFields(me.source?.fields ?: emptyList()) 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) { if(viewModel.avatarData.value == null) {
Glide.with(this) Glide.with(this)
@ -141,19 +142,19 @@ class EditProfileActivity : BaseActivity(), Injectable {
FitCenter(), FitCenter(),
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp)) RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp))
) )
.into(avatarPreview) .into(binding.avatarPreview)
} }
if(viewModel.headerData.value == null) { if(viewModel.headerData.value == null) {
Glide.with(this) Glide.with(this)
.load(me.header) .load(me.header)
.into(headerPreview) .into(binding.headerPreview)
} }
} }
} }
is Error -> { 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) { snackbar.setAction(R.string.action_retry) {
viewModel.obtainProfile() viewModel.obtainProfile()
} }
@ -169,14 +170,14 @@ class EditProfileActivity : BaseActivity(), Injectable {
is Success -> { is Success -> {
val instance = result.data val instance = result.data
if (instance?.maxBioChars != null && instance.maxBioChars > 0) { if (instance?.maxBioChars != null && instance.maxBioChars > 0) {
noteEditTextLayout.counterMaxLength = instance.maxBioChars binding.noteEditTextLayout.counterMaxLength = instance.maxBioChars
} }
} }
} }
} }
observeImage(viewModel.avatarData, avatarPreview, avatarProgressBar, true) observeImage(viewModel.avatarData, binding.avatarPreview, binding.avatarProgressBar, true)
observeImage(viewModel.headerData, headerPreview, headerProgressBar, false) observeImage(viewModel.headerData, binding.headerPreview, binding.headerProgressBar, false)
viewModel.saveData.observe(this, { viewModel.saveData.observe(this, {
when(it) { when(it) {
@ -184,7 +185,7 @@ class EditProfileActivity : BaseActivity(), Injectable {
finish() finish()
} }
is Loading -> { is Loading -> {
saveProgressBar.visibility = View.VISIBLE binding.saveProgressBar.visibility = View.VISIBLE
} }
is Error -> { is Error -> {
onSaveFailure(it.errorMessage) onSaveFailure(it.errorMessage)
@ -202,9 +203,9 @@ class EditProfileActivity : BaseActivity(), Injectable {
override fun onStop() { override fun onStop() {
super.onStop() super.onStop()
if(!isFinishing) { if(!isFinishing) {
viewModel.updateProfile(displayNameEditText.text.toString(), viewModel.updateProfile(binding.displayNameEditText.text.toString(),
noteEditText.text.toString(), binding.noteEditText.text.toString(),
lockedCheckBox.isChecked, binding.lockedCheckBox.isChecked,
accountFieldEditAdapter.getFieldData()) accountFieldEditAdapter.getFieldData())
} }
} }
@ -268,7 +269,7 @@ class EditProfileActivity : BaseActivity(), Injectable {
initiateMediaPicking() initiateMediaPicking()
} else { } else {
endMediaPicking() 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 return
} }
viewModel.save(displayNameEditText.text.toString(), viewModel.save(binding.displayNameEditText.text.toString(),
noteEditText.text.toString(), binding.noteEditText.text.toString(),
lockedCheckBox.isChecked, binding.lockedCheckBox.isChecked,
accountFieldEditAdapter.getFieldData(), accountFieldEditAdapter.getFieldData(),
this) this)
} }
private fun onSaveFailure(msg: String?) { private fun onSaveFailure(msg: String?) {
val errorMsg = msg ?: getString(R.string.error_media_upload_sending) val errorMsg = msg ?: getString(R.string.error_media_upload_sending)
Snackbar.make(avatarButton, errorMsg, Snackbar.LENGTH_LONG).show() Snackbar.make(binding.avatarButton, errorMsg, Snackbar.LENGTH_LONG).show()
saveProgressBar.visibility = View.GONE binding.saveProgressBar.visibility = View.GONE
} }
private fun beginMediaPicking() { private fun beginMediaPicking() {
when (currentlyPicking) { when (currentlyPicking) {
PickType.AVATAR -> { PickType.AVATAR -> {
avatarProgressBar.visibility = View.VISIBLE binding.avatarProgressBar.visibility = View.VISIBLE
avatarPreview.visibility = View.INVISIBLE binding.avatarPreview.visibility = View.INVISIBLE
avatarButton.setImageDrawable(null) binding.avatarButton.setImageDrawable(null)
} }
PickType.HEADER -> { PickType.HEADER -> {
headerProgressBar.visibility = View.VISIBLE binding.headerProgressBar.visibility = View.VISIBLE
headerPreview.visibility = View.INVISIBLE binding.headerPreview.visibility = View.INVISIBLE
headerButton.setImageDrawable(null) binding.headerButton.setImageDrawable(null)
} }
PickType.NOTHING -> { /* do nothing */ } PickType.NOTHING -> { /* do nothing */ }
} }
} }
private fun endMediaPicking() { private fun endMediaPicking() {
avatarProgressBar.visibility = View.GONE binding.avatarProgressBar.visibility = View.GONE
headerProgressBar.visibility = View.GONE binding.headerProgressBar.visibility = View.GONE
currentlyPicking = PickType.NOTHING currentlyPicking = PickType.NOTHING
} }
@ -402,7 +402,7 @@ class EditProfileActivity : BaseActivity(), Injectable {
} }
private fun onResizeFailure() { 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() endMediaPicking()
} }

View file

@ -7,13 +7,13 @@ import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent 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.entity.Filter
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.show
import kotlinx.android.synthetic.main.activity_filters.* import com.keylesspalace.tusky.util.viewBinding
import kotlinx.android.synthetic.main.dialog_filter.*
import kotlinx.android.synthetic.main.toolbar_basic.*
import okhttp3.ResponseBody import okhttp3.ResponseBody
import retrofit2.Call import retrofit2.Call
import retrofit2.Callback import retrofit2.Callback
@ -28,13 +28,28 @@ class FiltersActivity: BaseActivity() {
@Inject @Inject
lateinit var eventHub: EventHub lateinit var eventHub: EventHub
private val binding by viewBinding(ActivityFiltersBinding::inflate)
private lateinit var context : String private lateinit var context : String
private lateinit var filters: MutableList<Filter> private lateinit var filters: MutableList<Filter>
private lateinit var dialog: AlertDialog
companion object { override fun onCreate(savedInstanceState: Bundle?) {
const val FILTERS_CONTEXT = "filters_context" super.onCreate(savedInstanceState)
const val FILTERS_TITLE = "filters_title"
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) { private fun updateFilter(filter: Filter, itemIndex: Int) {
@ -101,52 +116,51 @@ class FiltersActivity: BaseActivity() {
} }
private fun showAddFilterDialog() { 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) .setTitle(R.string.filter_addition_dialog_title)
.setView(R.layout.dialog_filter) .setView(binding.root)
.setPositiveButton(android.R.string.ok){ _, _ -> .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) .setNeutralButton(android.R.string.cancel, null)
.create() .show()
dialog.show()
dialog.phraseWholeWord.isChecked = true
} }
private fun setupEditDialogForItem(itemIndex: Int) { 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) .setTitle(R.string.filter_edit_dialog_title)
.setView(R.layout.dialog_filter) .setView(binding.root)
.setPositiveButton(R.string.filter_dialog_update_button) { _, _ -> .setPositiveButton(R.string.filter_dialog_update_button) { _, _ ->
val oldFilter = filters[itemIndex] val oldFilter = filters[itemIndex]
val newFilter = Filter(oldFilter.id, dialog.phraseEditText.text.toString(), oldFilter.context, val newFilter = Filter(oldFilter.id, binding.phraseEditText.text.toString(), oldFilter.context,
oldFilter.expiresAt, oldFilter.irreversible, dialog.phraseWholeWord.isChecked) oldFilter.expiresAt, oldFilter.irreversible, binding.phraseWholeWord.isChecked)
updateFilter(newFilter, itemIndex) updateFilter(newFilter, itemIndex)
} }
.setNegativeButton(R.string.filter_dialog_remove_button) { _, _ -> .setNegativeButton(R.string.filter_dialog_remove_button) { _, _ ->
deleteFilter(itemIndex) deleteFilter(itemIndex)
} }
.setNeutralButton(android.R.string.cancel, null) .setNeutralButton(android.R.string.cancel, null)
.create() .show()
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
} }
private fun refreshFilterDisplay() { private fun refreshFilterDisplay() {
filtersView.adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, filters.map { filter -> filter.phrase }) binding.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.onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ -> setupEditDialogForItem(position) }
} }
private fun loadFilters() { private fun loadFilters() {
filterMessageView.hide() binding.filterMessageView.hide()
filtersView.hide() binding.filtersView.hide()
addFilterButton.hide() binding.addFilterButton.hide()
filterProgressBar.show() binding.filterProgressBar.show()
api.getFilters().enqueue(object : Callback<List<Filter>> { api.getFilters().enqueue(object : Callback<List<Filter>> {
override fun onResponse(call: Call<List<Filter>>, response: Response<List<Filter>>) { override fun onResponse(call: Call<List<Filter>>, response: Response<List<Filter>>) {
@ -156,52 +170,33 @@ class FiltersActivity: BaseActivity() {
filters = filterResponse.filter { filter -> filter.context.contains(context) }.toMutableList() filters = filterResponse.filter { filter -> filter.context.contains(context) }.toMutableList()
refreshFilterDisplay() refreshFilterDisplay()
filtersView.show() binding.filtersView.show()
addFilterButton.show() binding.addFilterButton.show()
filterProgressBar.hide() binding.filterProgressBar.hide()
} else { } else {
filterProgressBar.hide() binding.filterProgressBar.hide()
filterMessageView.show() binding.filterMessageView.show()
filterMessageView.setup(R.drawable.elephant_error, binding.filterMessageView.setup(R.drawable.elephant_error,
R.string.error_generic) { loadFilters() } R.string.error_generic) { loadFilters() }
} }
} }
override fun onFailure(call: Call<List<Filter>>, t: Throwable) { override fun onFailure(call: Call<List<Filter>>, t: Throwable) {
filterProgressBar.hide() binding.filterProgressBar.hide()
filterMessageView.show() binding.filterMessageView.show()
if (t is IOException) { if (t is IOException) {
filterMessageView.setup(R.drawable.elephant_offline, binding.filterMessageView.setup(R.drawable.elephant_offline,
R.string.error_network) { loadFilters() } R.string.error_network) { loadFilters() }
} else { } else {
filterMessageView.setup(R.drawable.elephant_error, binding.filterMessageView.setup(R.drawable.elephant_error,
R.string.error_generic) { loadFilters() } R.string.error_generic) { loadFilters() }
} }
} }
}) })
} }
override fun onCreate(savedInstanceState: Bundle?) { companion object {
super.onCreate(savedInstanceState) const val FILTERS_CONTEXT = "filters_context"
const val FILTERS_TITLE = "filters_title"
setContentView(R.layout.activity_filters)
setupToolbarBackArrow()
addFilterButton.setOnClickListener {
showAddFilterDialog()
}
title = intent?.getStringExtra(FILTERS_TITLE)
context = intent?.getStringExtra(FILTERS_CONTEXT)!!
loadFilters()
} }
private fun setupToolbarBackArrow() {
setSupportActionBar(toolbar)
supportActionBar?.run {
// Back button
setDisplayHomeAsUpEnabled(true)
setDisplayShowHomeEnabled(true)
}
}
} }

View file

@ -19,23 +19,20 @@ import android.os.Bundle
import androidx.annotation.RawRes import androidx.annotation.RawRes
import android.util.Log import android.util.Log
import android.widget.TextView import android.widget.TextView
import com.keylesspalace.tusky.databinding.ActivityLicenseBinding
import com.keylesspalace.tusky.util.IOUtils 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.BufferedReader
import java.io.IOException import java.io.IOException
import java.io.InputStreamReader import java.io.InputStreamReader
class LicenseActivity : BaseActivity() { class LicenseActivity : BaseActivity() {
@ContainerOptions(cache = CacheImplementation.NO_CACHE)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_license) val binding = ActivityLicenseBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(toolbar) setSupportActionBar(binding.includedToolbar.toolbar)
supportActionBar?.run { supportActionBar?.run {
setDisplayHomeAsUpEnabled(true) setDisplayHomeAsUpEnabled(true)
setDisplayShowHomeEnabled(true) setDisplayShowHomeEnabled(true)
@ -43,7 +40,7 @@ class LicenseActivity : BaseActivity() {
setTitle(R.string.title_licenses) 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) IOUtils.closeQuietly(br)
textView.text = sb.toString() textView.text = sb.toString()
} }
} }

View file

@ -24,12 +24,14 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.* import android.widget.*
import androidx.activity.viewModels
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.* import androidx.recyclerview.widget.*
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import at.connyduck.sparkbutton.helpers.Utils import at.connyduck.sparkbutton.helpers.Utils
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.databinding.ActivityListsBinding
import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.entity.MastoList import com.keylesspalace.tusky.entity.MastoList
@ -47,8 +49,6 @@ import com.uber.autodispose.autoDispose
import dagger.android.DispatchingAndroidInjector import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector import dagger.android.HasAndroidInjector
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.synthetic.main.activity_lists.*
import kotlinx.android.synthetic.main.toolbar_basic.*
import javax.inject.Inject import javax.inject.Inject
/** /**
@ -57,47 +57,42 @@ import javax.inject.Inject
class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector { class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
companion object {
@JvmStatic
fun newIntent(context: Context): Intent {
return Intent(context, ListsActivity::class.java)
}
}
@Inject @Inject
lateinit var viewModelFactory: ViewModelFactory lateinit var viewModelFactory: ViewModelFactory
@Inject @Inject
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Any> lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Any>
private lateinit var viewModel: ListsViewModel private val viewModel: ListsViewModel by viewModels { viewModelFactory }
private val binding by viewBinding(ActivityListsBinding::inflate)
private val adapter = ListsAdapter() private val adapter = ListsAdapter()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_lists)
setContentView(binding.root)
setSupportActionBar(toolbar) setSupportActionBar(binding.includedToolbar.toolbar)
supportActionBar?.apply { supportActionBar?.apply {
title = getString(R.string.title_lists) title = getString(R.string.title_lists)
setDisplayHomeAsUpEnabled(true) setDisplayHomeAsUpEnabled(true)
setDisplayShowHomeEnabled(true) setDisplayShowHomeEnabled(true)
} }
listsRecycler.adapter = adapter binding.listsRecycler.adapter = adapter
listsRecycler.layoutManager = LinearLayoutManager(this) binding.listsRecycler.layoutManager = LinearLayoutManager(this)
listsRecycler.addItemDecoration( binding.listsRecycler.addItemDecoration(
DividerItemDecoration(this, DividerItemDecoration.VERTICAL)) DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
viewModel = viewModelFactory.create(ListsViewModel::class.java)
viewModel.state viewModel.state
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.autoDispose(from(this)) .autoDispose(from(this))
.subscribe(this::update) .subscribe(this::update)
viewModel.retryLoading() viewModel.retryLoading()
addListButton.setOnClickListener { binding.addListButton.setOnClickListener {
showlistNameDialog(null) showlistNameDialog(null)
} }
@ -153,37 +148,36 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
private fun update(state: ListsViewModel.State) { private fun update(state: ListsViewModel.State) {
adapter.submitList(state.lists) adapter.submitList(state.lists)
progressBar.visible(state.loadingState == LOADING) binding.progressBar.visible(state.loadingState == LOADING)
when (state.loadingState) { when (state.loadingState) {
INITIAL, LOADING -> messageView.hide() INITIAL, LOADING -> binding.messageView.hide()
ERROR_NETWORK -> { ERROR_NETWORK -> {
messageView.show() binding.messageView.show()
messageView.setup(R.drawable.elephant_offline, R.string.error_network) { binding.messageView.setup(R.drawable.elephant_offline, R.string.error_network) {
viewModel.retryLoading() viewModel.retryLoading()
} }
} }
ERROR_OTHER -> { ERROR_OTHER -> {
messageView.show() binding.messageView.show()
messageView.setup(R.drawable.elephant_error, R.string.error_generic) { binding.messageView.setup(R.drawable.elephant_error, R.string.error_generic) {
viewModel.retryLoading() viewModel.retryLoading()
} }
} }
LOADED -> LOADED ->
if (state.lists.isEmpty()) { if (state.lists.isEmpty()) {
messageView.show() binding.messageView.show()
messageView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, binding.messageView.setup(R.drawable.elephant_friend_empty, R.string.message_empty,
null) null)
} else { } else {
messageView.hide() binding.messageView.hide()
} }
} }
} }
private fun showMessage(@StringRes messageId: Int) { private fun showMessage(@StringRes messageId: Int) {
Snackbar.make( Snackbar.make(
listsRecycler, messageId, Snackbar.LENGTH_SHORT binding.listsRecycler, messageId, Snackbar.LENGTH_SHORT
).show() ).show()
} }
private fun onListSelected(listId: String) { private fun onListSelected(listId: String) {
@ -215,8 +209,6 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
} }
} }
override fun androidInjector() = dispatchingAndroidInjector
private object ListsDiffer : DiffUtil.ItemCallback<MastoList>() { private object ListsDiffer : DiffUtil.ItemCallback<MastoList>() {
override fun areItemsTheSame(oldItem: MastoList, newItem: MastoList): Boolean { override fun areItemsTheSame(oldItem: MastoList, newItem: MastoList): Boolean {
return oldItem.id == newItem.id return oldItem.id == newItem.id
@ -258,9 +250,9 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
override fun onClick(v: View) { override fun onClick(v: View) {
if (v == itemView) { if (v == itemView) {
onListSelected(getItem(adapterPosition).id) onListSelected(getItem(bindingAdapterPosition).id)
} else { } else {
onMore(getItem(adapterPosition), v) onMore(getItem(bindingAdapterPosition), v)
} }
} }
} }
@ -273,4 +265,10 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
viewModel.renameList(listId, name.toString()) viewModel.renameList(listId, name.toString())
} }
} }
override fun androidInjector() = dispatchingAndroidInjector
companion object {
fun newIntent(context: Context) = Intent(context, ListsActivity::class.java)
}
} }

View file

@ -29,15 +29,12 @@ import androidx.appcompat.app.AlertDialog
import androidx.browser.customtabs.CustomTabColorSchemeParams import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.keylesspalace.tusky.databinding.ActivityLoginBinding
import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.entity.AccessToken import com.keylesspalace.tusky.entity.AccessToken
import com.keylesspalace.tusky.entity.AppCredentials import com.keylesspalace.tusky.entity.AppCredentials
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.ThemeUtils import com.keylesspalace.tusky.util.*
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 okhttp3.HttpUrl import okhttp3.HttpUrl
import retrofit2.Call import retrofit2.Call
import retrofit2.Callback import retrofit2.Callback
@ -49,6 +46,8 @@ class LoginActivity : BaseActivity(), Injectable {
@Inject @Inject
lateinit var mastodonApi: MastodonApi lateinit var mastodonApi: MastodonApi
private val binding by viewBinding(ActivityLoginBinding::inflate)
private lateinit var preferences: SharedPreferences private lateinit var preferences: SharedPreferences
private val oauthRedirectUri: String private val oauthRedirectUri: String
@ -61,27 +60,27 @@ class LoginActivity : BaseActivity(), Injectable {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login) setContentView(binding.root)
if(savedInstanceState == null && BuildConfig.CUSTOM_INSTANCE.isNotBlank() && !isAdditionalLogin()) { if(savedInstanceState == null && BuildConfig.CUSTOM_INSTANCE.isNotBlank() && !isAdditionalLogin()) {
domainEditText.setText(BuildConfig.CUSTOM_INSTANCE) binding.domainEditText.setText(BuildConfig.CUSTOM_INSTANCE)
domainEditText.setSelection(BuildConfig.CUSTOM_INSTANCE.length) binding.domainEditText.setSelection(BuildConfig.CUSTOM_INSTANCE.length)
} }
if(BuildConfig.CUSTOM_LOGO_URL.isNotBlank()) { if(BuildConfig.CUSTOM_LOGO_URL.isNotBlank()) {
Glide.with(loginLogo) Glide.with(binding.loginLogo)
.load(BuildConfig.CUSTOM_LOGO_URL) .load(BuildConfig.CUSTOM_LOGO_URL)
.placeholder(null) .placeholder(null)
.into(loginLogo) .into(binding.loginLogo)
} }
preferences = getSharedPreferences( preferences = getSharedPreferences(
getString(R.string.preferences_file_key), Context.MODE_PRIVATE) getString(R.string.preferences_file_key), Context.MODE_PRIVATE)
loginButton.setOnClickListener { onButtonClick() } binding.loginButton.setOnClickListener { onButtonClick() }
registerButton.setOnClickListener { onRegisterClick() } binding.registerButton.setOnClickListener { onRegisterClick() }
whatsAnInstanceTextView.setOnClickListener { binding.whatsAnInstanceTextView.setOnClickListener {
val dialog = AlertDialog.Builder(this) val dialog = AlertDialog.Builder(this)
.setMessage(R.string.dialog_whats_an_instance) .setMessage(R.string.dialog_whats_an_instance)
.setPositiveButton(R.string.action_close, null) .setPositiveButton(R.string.action_close, null)
@ -91,11 +90,11 @@ class LoginActivity : BaseActivity(), Injectable {
} }
if (isAdditionalLogin()) { if (isAdditionalLogin()) {
setSupportActionBar(toolbar) setSupportActionBar(binding.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowTitleEnabled(false) supportActionBar?.setDisplayShowTitleEnabled(false)
} else { } else {
toolbar.visibility = View.GONE binding.toolbar.visibility = View.GONE
} }
} }
@ -112,7 +111,7 @@ class LoginActivity : BaseActivity(), Injectable {
} }
private fun onRegisterClick() { private fun onRegisterClick() {
registerButton.isEnabled = false binding.registerButton.isEnabled = false
val uri = Uri.parse(BuildConfig.REGISTER_ACCOUNT_URL) val uri = Uri.parse(BuildConfig.REGISTER_ACCOUNT_URL)
if (!openInCustomTab(uri, this)) { if (!openInCustomTab(uri, this)) {
@ -120,7 +119,7 @@ class LoginActivity : BaseActivity(), Injectable {
if (viewIntent.resolveActivity(packageManager) != null) { if (viewIntent.resolveActivity(packageManager) != null) {
startActivity(viewIntent) startActivity(viewIntent)
} else { } else {
domainEditText.error = getString(R.string.error_no_web_browser_found) binding.domainEditText.error = getString(R.string.error_no_web_browser_found)
setLoading(false) setLoading(false)
} }
} }
@ -133,15 +132,15 @@ class LoginActivity : BaseActivity(), Injectable {
*/ */
private fun onButtonClick() { private fun onButtonClick() {
loginButton.isEnabled = false binding.loginButton.isEnabled = false
val domain = canonicalizeDomain(domainEditText.text.toString()) val domain = canonicalizeDomain(binding.domainEditText.text.toString())
try { try {
HttpUrl.Builder().host(domain).scheme("https").build() HttpUrl.Builder().host(domain).scheme("https").build()
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
setLoading(false) setLoading(false)
domainTextInputLayout.error = getString(R.string.error_invalid_domain) binding.domainTextInputLayout.error = getString(R.string.error_invalid_domain)
return return
} }
@ -154,8 +153,8 @@ class LoginActivity : BaseActivity(), Injectable {
override fun onResponse(call: Call<AppCredentials>, override fun onResponse(call: Call<AppCredentials>,
response: Response<AppCredentials>) { response: Response<AppCredentials>) {
if (!response.isSuccessful) { if (!response.isSuccessful) {
loginButton.isEnabled = true binding.loginButton.isEnabled = true
domainTextInputLayout.error = getString(R.string.error_failed_app_registration) binding.domainTextInputLayout.error = getString(R.string.error_failed_app_registration)
setLoading(false) setLoading(false)
Log.e(TAG, "App authentication failed. " + response.message()) Log.e(TAG, "App authentication failed. " + response.message())
return return
@ -174,8 +173,8 @@ class LoginActivity : BaseActivity(), Injectable {
} }
override fun onFailure(call: Call<AppCredentials>, t: Throwable) { override fun onFailure(call: Call<AppCredentials>, t: Throwable) {
loginButton.isEnabled = true binding.loginButton.isEnabled = true
domainTextInputLayout.error = getString(R.string.error_failed_app_registration) binding.domainTextInputLayout.error = getString(R.string.error_failed_app_registration)
setLoading(false) setLoading(false)
Log.e(TAG, Log.getStackTraceString(t)) Log.e(TAG, Log.getStackTraceString(t))
} }
@ -206,7 +205,7 @@ class LoginActivity : BaseActivity(), Injectable {
if (viewIntent.resolveActivity(packageManager) != null) { if (viewIntent.resolveActivity(packageManager) != null) {
startActivity(viewIntent) startActivity(viewIntent)
} else { } else {
domainEditText.error = getString(R.string.error_no_web_browser_found) binding.domainEditText.error = getString(R.string.error_no_web_browser_found)
setLoading(false) setLoading(false)
} }
} }
@ -240,7 +239,7 @@ class LoginActivity : BaseActivity(), Injectable {
onLoginSuccess(response.body()!!.accessToken, domain) onLoginSuccess(response.body()!!.accessToken, domain)
} else { } else {
setLoading(false) 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", Log.e(TAG, String.format("%s %s",
getString(R.string.error_retrieving_oauth_token), getString(R.string.error_retrieving_oauth_token),
response.message())) response.message()))
@ -249,7 +248,7 @@ class LoginActivity : BaseActivity(), Injectable {
override fun onFailure(call: Call<AccessToken>, t: Throwable) { override fun onFailure(call: Call<AccessToken>, t: Throwable) {
setLoading(false) 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", Log.e(TAG, String.format("%s %s",
getString(R.string.error_retrieving_oauth_token), getString(R.string.error_retrieving_oauth_token),
t.message)) t.message))
@ -262,14 +261,14 @@ class LoginActivity : BaseActivity(), Injectable {
/* Authorization failed. Put the error response where the user can read it and they /* Authorization failed. Put the error response where the user can read it and they
* can try again. */ * can try again. */
setLoading(false) 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", Log.e(TAG, String.format("%s %s",
getString(R.string.error_authorization_denied), getString(R.string.error_authorization_denied),
error)) error))
} else { } else {
// This case means a junk response was received somehow. // This case means a junk response was received somehow.
setLoading(false) setLoading(false)
domainTextInputLayout.error = getString(R.string.error_authorization_unknown) binding.domainTextInputLayout.error = getString(R.string.error_authorization_unknown)
} }
} else { } else {
// first show or user cancelled login // first show or user cancelled login
@ -279,12 +278,12 @@ class LoginActivity : BaseActivity(), Injectable {
private fun setLoading(loadingState: Boolean) { private fun setLoading(loadingState: Boolean) {
if (loadingState) { if (loadingState) {
loginLoadingLayout.visibility = View.VISIBLE binding.loginLoadingLayout.visibility = View.VISIBLE
loginInputLayout.visibility = View.GONE binding.loginInputLayout.visibility = View.GONE
} else { } else {
loginLoadingLayout.visibility = View.GONE binding.loginLoadingLayout.visibility = View.GONE
loginInputLayout.visibility = View.VISIBLE binding.loginInputLayout.visibility = View.VISIBLE
loginButton.isEnabled = true binding.loginButton.isEnabled = true
} }
} }

View file

@ -59,6 +59,7 @@ import com.keylesspalace.tusky.components.notifications.NotificationHelper
import com.keylesspalace.tusky.components.preference.PreferencesActivity import com.keylesspalace.tusky.components.preference.PreferencesActivity
import com.keylesspalace.tusky.components.scheduled.ScheduledTootActivity import com.keylesspalace.tusky.components.scheduled.ScheduledTootActivity
import com.keylesspalace.tusky.components.search.SearchActivity import com.keylesspalace.tusky.components.search.SearchActivity
import com.keylesspalace.tusky.databinding.ActivityMainBinding
import com.keylesspalace.tusky.db.AccountEntity import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Account
@ -86,7 +87,6 @@ import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector import dagger.android.HasAndroidInjector
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
import kotlinx.android.synthetic.main.activity_main.*
import javax.inject.Inject import javax.inject.Inject
class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInjector { class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInjector {
@ -108,6 +108,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
@Inject @Inject
lateinit var draftHelper: DraftHelper lateinit var draftHelper: DraftHelper
private val binding by viewBinding(ActivityMainBinding::inflate)
private lateinit var header: AccountHeaderView private lateinit var header: AccountHeaderView
private var notificationTabPosition = 0 private var notificationTabPosition = 0
@ -119,6 +121,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
private lateinit var glide: RequestManager private lateinit var glide: RequestManager
private var accountLocked: Boolean = false
private val emojiInitCallback = object : InitCallback() { private val emojiInitCallback = object : InitCallback() {
override fun onInitialized() { override fun onInitialized() {
if (!isDestroyed) { if (!isDestroyed) {
@ -173,27 +177,27 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
} }
}) })
} }
} else if (accountRequested) { } else if (accountRequested && savedInstanceState == null) {
// user clicked a notification, show notification tab and switch user if necessary // user clicked a notification, show notification tab
showNotificationTab = true showNotificationTab = true
} }
} }
window.statusBarColor = Color.TRANSPARENT // don't draw a status bar, the DrawerLayout and the MaterialDrawerLayout have their own 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) glide = Glide.with(this)
composeButton.setOnClickListener { binding.composeButton.setOnClickListener {
val composeIntent = Intent(applicationContext, ComposeActivity::class.java) val composeIntent = Intent(applicationContext, ComposeActivity::class.java)
startActivity(composeIntent) startActivity(composeIntent)
} }
val hideTopToolbar = preferences.getBoolean(PrefKeys.HIDE_TOP_TOOLBAR, false) val hideTopToolbar = preferences.getBoolean(PrefKeys.HIDE_TOP_TOOLBAR, false)
mainToolbar.visible(!hideTopToolbar) binding.mainToolbar.visible(!hideTopToolbar)
loadDrawerAvatar(activeAccount.profilePictureUrl, true) 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) setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
icon = IconicsDrawable(this@MainActivity, GoogleMaterial.Icon.gmd_search).apply { icon = IconicsDrawable(this@MainActivity, GoogleMaterial.Icon.gmd_search).apply {
sizeDp = 20 sizeDp = 20
@ -249,11 +253,11 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
override fun onBackPressed() { override fun onBackPressed() {
when { when {
mainDrawerLayout.isOpen -> { binding.mainDrawerLayout.isOpen -> {
mainDrawerLayout.close() binding.mainDrawerLayout.close()
} }
viewPager.currentItem != 0 -> { binding.viewPager.currentItem != 0 -> {
viewPager.currentItem = 0 binding.viewPager.currentItem = 0
} }
else -> { else -> {
super.onBackPressed() super.onBackPressed()
@ -264,10 +268,10 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
when (keyCode) { when (keyCode) {
KeyEvent.KEYCODE_MENU -> { KeyEvent.KEYCODE_MENU -> {
if (mainDrawerLayout.isOpen) { if (binding.mainDrawerLayout.isOpen) {
mainDrawerLayout.close() binding.mainDrawerLayout.close()
} else { } else {
mainDrawerLayout.open() binding.mainDrawerLayout.open()
} }
return true return true
} }
@ -319,8 +323,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
private fun setupDrawer(savedInstanceState: Bundle?, addSearchButton: Boolean) { private fun setupDrawer(savedInstanceState: Bundle?, addSearchButton: Boolean) {
mainToolbar.setNavigationOnClickListener { binding.mainToolbar.setNavigationOnClickListener {
mainDrawerLayout.open() binding.mainDrawerLayout.open()
} }
header = AccountHeaderView(this).apply { header = AccountHeaderView(this).apply {
@ -333,7 +337,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
descriptionRes = R.string.add_account_description descriptionRes = R.string.add_account_description
iconicsIcon = GoogleMaterial.Icon.gmd_add iconicsIcon = GoogleMaterial.Icon.gmd_add
}, 0) }, 0)
attachToSliderView(mainDrawer) attachToSliderView(binding.mainDrawer)
dividerBelowHeader = false dividerBelowHeader = false
closeDrawerOnProfileListClick = true closeDrawerOnProfileListClick = true
} }
@ -369,7 +373,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
} }
}) })
mainDrawer.apply { binding.mainDrawer.apply {
tintStatusBar = true tintStatusBar = true
addItems( addItems(
primaryDrawerItem { primaryDrawerItem {
@ -397,6 +401,14 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
startActivityWithSlideInAnimation(intent) 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 { primaryDrawerItem {
nameRes = R.string.action_lists nameRes = R.string.action_lists
iconicsIcon = GoogleMaterial.Icon.gmd_list iconicsIcon = GoogleMaterial.Icon.gmd_list
@ -464,7 +476,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
) )
if (addSearchButton) { if (addSearchButton) {
mainDrawer.addItemsAtPosition(4, binding.mainDrawer.addItemsAtPosition(4,
primaryDrawerItem { primaryDrawerItem {
nameRes = R.string.action_search nameRes = R.string.action_search
iconicsIcon = GoogleMaterial.Icon.gmd_search iconicsIcon = GoogleMaterial.Icon.gmd_search
@ -478,7 +490,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
} }
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
mainDrawer.addItems( binding.mainDrawer.addItems(
secondaryDrawerItem { secondaryDrawerItem {
nameText = "debug" nameText = "debug"
isEnabled = false isEnabled = false
@ -490,7 +502,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(mainDrawer.saveInstanceState(outState)) super.onSaveInstanceState(binding.mainDrawer.saveInstanceState(outState))
} }
private fun setupTabs(selectNotificationTab: Boolean) { private fun setupTabs(selectNotificationTab: Boolean) {
@ -498,21 +510,21 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
val activeTabLayout = if (preferences.getString("mainNavPosition", "top") == "bottom") { val activeTabLayout = if (preferences.getString("mainNavPosition", "top") == "bottom") {
val actionBarSize = ThemeUtils.getDimension(this, R.attr.actionBarSize) val actionBarSize = ThemeUtils.getDimension(this, R.attr.actionBarSize)
val fabMargin = resources.getDimensionPixelSize(R.dimen.fabMargin) val fabMargin = resources.getDimensionPixelSize(R.dimen.fabMargin)
(composeButton.layoutParams as CoordinatorLayout.LayoutParams).bottomMargin = actionBarSize + fabMargin (binding.composeButton.layoutParams as CoordinatorLayout.LayoutParams).bottomMargin = actionBarSize + fabMargin
tabLayout.hide() binding.tabLayout.hide()
bottomTabLayout binding.bottomTabLayout
} else { } else {
bottomNav.hide() binding.bottomNav.hide()
(viewPager.layoutParams as CoordinatorLayout.LayoutParams).bottomMargin = 0 (binding.viewPager.layoutParams as CoordinatorLayout.LayoutParams).bottomMargin = 0
(composeButton.layoutParams as CoordinatorLayout.LayoutParams).anchorId = R.id.viewPager (binding.composeButton.layoutParams as CoordinatorLayout.LayoutParams).anchorId = R.id.viewPager
tabLayout binding.tabLayout
} }
val tabs = accountManager.activeAccount!!.tabPreferences val tabs = accountManager.activeAccount!!.tabPreferences
val adapter = MainPagerAdapter(tabs, this) val adapter = MainPagerAdapter(tabs, this)
viewPager.adapter = adapter binding.viewPager.adapter = adapter
TabLayoutMediator(activeTabLayout, viewPager) { _: TabLayout.Tab?, _: Int -> }.attach() TabLayoutMediator(activeTabLayout, binding.viewPager) { _: TabLayout.Tab?, _: Int -> }.attach()
activeTabLayout.removeAllTabs() activeTabLayout.removeAllTabs()
for (i in tabs.indices) { for (i in tabs.indices) {
val tab = activeTabLayout.newTab() val tab = activeTabLayout.newTab()
@ -533,10 +545,10 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
} }
val pageMargin = resources.getDimensionPixelSize(R.dimen.tab_page_margin) val pageMargin = resources.getDimensionPixelSize(R.dimen.tab_page_margin)
viewPager.setPageTransformer(MarginPageTransformer(pageMargin)) binding.viewPager.setPageTransformer(MarginPageTransformer(pageMargin))
val enableSwipeForTabs = preferences.getBoolean("enableSwipeForTabs", true) val enableSwipeForTabs = preferences.getBoolean("enableSwipeForTabs", true)
viewPager.isUserInputEnabled = enableSwipeForTabs binding.viewPager.isUserInputEnabled = enableSwipeForTabs
onTabSelectedListener?.let { onTabSelectedListener?.let {
activeTabLayout.removeOnTabSelectedListener(it) activeTabLayout.removeOnTabSelectedListener(it)
@ -548,7 +560,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
NotificationHelper.clearNotificationsForActiveAccount(this@MainActivity, accountManager) 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) {} override fun onTabUnselected(tab: TabLayout.Tab) {}
@ -564,8 +576,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
} }
val activeTabPosition = if (selectNotificationTab) notificationTabPosition else 0 val activeTabPosition = if (selectNotificationTab) notificationTabPosition else 0
mainToolbar.title = tabs[activeTabPosition].title(this@MainActivity) binding.mainToolbar.title = tabs[activeTabPosition].title(this@MainActivity)
mainToolbar.setOnClickListener { binding.mainToolbar.setOnClickListener {
(adapter.getFragment(activeTabLayout.selectedTabPosition) as? ReselectableFragment)?.onReselect() (adapter.getFragment(activeTabLayout.selectedTabPosition) as? ReselectableFragment)?.onReselect()
} }
@ -658,22 +670,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
accountManager.updateActiveAccount(me) accountManager.updateActiveAccount(me)
NotificationHelper.createNotificationChannelsForAccount(accountManager.activeAccount!!, this) NotificationHelper.createNotificationChannelsForAccount(accountManager.activeAccount!!, this)
// Show follow requests in the menu, if this is a locked account. accountLocked = me.locked
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)
}
updateProfiles() updateProfiles()
updateShortcut(this, accountManager.activeAccount!!) updateShortcut(this, accountManager.activeAccount!!)
} }
@ -684,7 +682,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
glide.asDrawable() glide.asDrawable()
.load(avatarUrl) .load(avatarUrl)
.transform( .transform(
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp)) RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp))
) )
.apply { .apply {
if (showPlaceholder) { if (showPlaceholder) {
@ -695,16 +693,17 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
override fun onLoadStarted(placeholder: Drawable?) { override fun onLoadStarted(placeholder: Drawable?) {
if (placeholder != null) { if (placeholder != null) {
mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize) binding.mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize)
} }
} }
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) { override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
mainToolbar.navigationIcon = FixedSizeDrawable(resource, navIconSize, navIconSize) binding.mainToolbar.navigationIcon = FixedSizeDrawable(resource, navIconSize, navIconSize)
} }
override fun onLoadCleared(placeholder: Drawable?) { override fun onLoadCleared(placeholder: Drawable?) {
if (placeholder != null) { 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() { 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() { 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 override fun androidInjector() = androidInjector
companion object { companion object {
private const val TAG = "MainActivity" // logging tag private const val TAG = "MainActivity" // logging tag
private const val DRAWER_ITEM_ADD_ACCOUNT: Long = -13 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 private const val DRAWER_ITEM_ANNOUNCEMENTS: Long = 14
const val STATUS_URL = "statusUrl" const val STATUS_URL = "statusUrl"
} }

View file

@ -4,43 +4,28 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.keylesspalace.tusky.databinding.ActivityModalTimelineBinding
import com.keylesspalace.tusky.fragment.TimelineFragment import com.keylesspalace.tusky.fragment.TimelineFragment
import com.keylesspalace.tusky.interfaces.ActionButtonActivity import com.keylesspalace.tusky.interfaces.ActionButtonActivity
import dagger.android.DispatchingAndroidInjector import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector import dagger.android.HasAndroidInjector
import kotlinx.android.synthetic.main.toolbar_basic.*
import javax.inject.Inject import javax.inject.Inject
class ModalTimelineActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInjector { 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 @Inject
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Any> lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Any>
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_modal_timeline) val binding = ActivityModalTimelineBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(toolbar) setSupportActionBar(binding.includedToolbar.toolbar)
val bar = supportActionBar supportActionBar?.apply {
if (bar != null) { title = getString(R.string.title_list_timeline)
bar.title = getString(R.string.title_list_timeline) setDisplayHomeAsUpEnabled(true)
bar.setDisplayHomeAsUpEnabled(true) setDisplayShowHomeEnabled(true)
bar.setDisplayShowHomeEnabled(true)
} }
if (supportFragmentManager.findFragmentById(R.id.contentFrame) == null) { if (supportFragmentManager.findFragmentById(R.id.contentFrame) == null) {
@ -57,4 +42,18 @@ class ModalTimelineActivity : BottomSheetActivity(), ActionButtonActivity, HasAn
override fun androidInjector() = dispatchingAndroidInjector 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
}
}
} }

View file

@ -19,6 +19,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.commit import androidx.fragment.app.commit
import com.keylesspalace.tusky.databinding.ActivityStatuslistBinding
import com.keylesspalace.tusky.fragment.TimelineFragment import com.keylesspalace.tusky.fragment.TimelineFragment
import com.keylesspalace.tusky.fragment.TimelineFragment.Kind import com.keylesspalace.tusky.fragment.TimelineFragment.Kind
@ -27,9 +28,6 @@ import javax.inject.Inject
import dagger.android.DispatchingAndroidInjector import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector 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 { class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
@ -39,12 +37,12 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
private val kind: Kind private val kind: Kind
get() = Kind.valueOf(intent.getStringExtra(EXTRA_KIND)!!) get() = Kind.valueOf(intent.getStringExtra(EXTRA_KIND)!!)
@ContainerOptions(cache = CacheImplementation.NO_CACHE)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) 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) { val title = if(kind == Kind.FAVOURITES) {
R.string.title_favourites R.string.title_favourites

View file

@ -38,17 +38,17 @@ import com.keylesspalace.tusky.adapter.ListSelectionAdapter
import com.keylesspalace.tusky.adapter.TabAdapter import com.keylesspalace.tusky.adapter.TabAdapter
import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.MainTabsChangedEvent import com.keylesspalace.tusky.appstore.MainTabsChangedEvent
import com.keylesspalace.tusky.databinding.ActivityTabPreferenceBinding
import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.onTextChanged import com.keylesspalace.tusky.util.onTextChanged
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.util.visible import com.keylesspalace.tusky.util.visible
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from
import com.uber.autodispose.autoDispose import com.uber.autodispose.autoDispose
import io.reactivex.Single import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers 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 java.util.regex.Pattern
import javax.inject.Inject import javax.inject.Inject
@ -59,6 +59,8 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
@Inject @Inject
lateinit var eventHub: EventHub lateinit var eventHub: EventHub
private val binding by viewBinding(ActivityTabPreferenceBinding::inflate)
private lateinit var currentTabs: MutableList<TabData> private lateinit var currentTabs: MutableList<TabData>
private lateinit var currentTabsAdapter: TabAdapter private lateinit var currentTabsAdapter: TabAdapter
private lateinit var touchHelper: ItemTouchHelper private lateinit var touchHelper: ItemTouchHelper
@ -73,9 +75,9 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_tab_preference) setContentView(binding.root)
setSupportActionBar(toolbar) setSupportActionBar(binding.includedToolbar.toolbar)
supportActionBar?.apply { supportActionBar?.apply {
setTitle(R.string.title_tab_preferences) setTitle(R.string.title_tab_preferences)
@ -85,13 +87,13 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
currentTabs = accountManager.activeAccount?.tabPreferences.orEmpty().toMutableList() currentTabs = accountManager.activeAccount?.tabPreferences.orEmpty().toMutableList()
currentTabsAdapter = TabAdapter(currentTabs, false, this, currentTabs.size <= MIN_TAB_COUNT) currentTabsAdapter = TabAdapter(currentTabs, false, this, currentTabs.size <= MIN_TAB_COUNT)
currentTabsRecyclerView.adapter = currentTabsAdapter binding.currentTabsRecyclerView.adapter = currentTabsAdapter
currentTabsRecyclerView.layoutManager = LinearLayoutManager(this) binding.currentTabsRecyclerView.layoutManager = LinearLayoutManager(this)
currentTabsRecyclerView.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL)) binding.currentTabsRecyclerView.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL))
addTabAdapter = TabAdapter(listOf(createTabDataFromId(DIRECT)), true, this) addTabAdapter = TabAdapter(listOf(createTabDataFromId(DIRECT)), true, this)
addTabRecyclerView.adapter = addTabAdapter binding.addTabRecyclerView.adapter = addTabAdapter
addTabRecyclerView.layoutManager = LinearLayoutManager(this) binding.addTabRecyclerView.layoutManager = LinearLayoutManager(this)
touchHelper = ItemTouchHelper(object : ItemTouchHelper.Callback() { touchHelper = ItemTouchHelper(object : ItemTouchHelper.Callback() {
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int { 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 { override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
val temp = currentTabs[viewHolder.adapterPosition] val temp = currentTabs[viewHolder.bindingAdapterPosition]
currentTabs[viewHolder.adapterPosition] = currentTabs[target.adapterPosition] currentTabs[viewHolder.bindingAdapterPosition] = currentTabs[target.bindingAdapterPosition]
currentTabs[target.adapterPosition] = temp currentTabs[target.bindingAdapterPosition] = temp
currentTabsAdapter.notifyItemMoved(viewHolder.adapterPosition, target.adapterPosition) currentTabsAdapter.notifyItemMoved(viewHolder.bindingAdapterPosition, target.bindingAdapterPosition)
saveTabs() saveTabs()
return true return true
} }
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
onTabRemoved(viewHolder.adapterPosition) onTabRemoved(viewHolder.bindingAdapterPosition)
} }
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { 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) toggleFab(true)
} }
scrim.setOnClickListener { binding.scrim.setOnClickListener {
toggleFab(false) 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() updateAvailableTabs()
} }
@ -193,18 +195,18 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
private fun toggleFab(expand: Boolean) { private fun toggleFab(expand: Boolean) {
val transition = MaterialContainerTransform().apply { val transition = MaterialContainerTransform().apply {
startView = if (expand) actionButton else sheet startView = if (expand) binding.actionButton else binding.sheet
val endView: View = if (expand) sheet else actionButton val endView: View = if (expand) binding.sheet else binding.actionButton
this.endView = endView this.endView = endView
addTarget(endView) addTarget(endView)
scrimColor = Color.TRANSPARENT scrimColor = Color.TRANSPARENT
setPathMotion(MaterialArcMotion()) setPathMotion(MaterialArcMotion())
} }
TransitionManager.beginDelayedTransition(tabPreferenceContainer, transition) TransitionManager.beginDelayedTransition(binding.root, transition)
actionButton.visible(!expand) binding.actionButton.visible(!expand)
sheet.visible(expand) binding.sheet.visible(expand)
scrim.visible(expand) binding.scrim.visible(expand)
} }
private fun showAddHashtagDialog(tab: TabData? = null, tabPosition: Int = 0) { private fun showAddHashtagDialog(tab: TabData? = null, tabPosition: Int = 0) {
@ -310,7 +312,7 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
addTabAdapter.updateData(addableTabs) 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) currentTabsAdapter.setRemoveButtonVisible(currentTabs.size > MIN_TAB_COUNT)
} }
@ -337,7 +339,7 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
} }
override fun onBackPressed() { override fun onBackPressed() {
if (actionButton.isVisible) { if (binding.actionButton.isVisible) {
super.onBackPressed() super.onBackPressed()
} else { } else {
toggleFab(false) toggleFab(false)

View file

@ -44,18 +44,19 @@ import androidx.viewpager2.widget.ViewPager2
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.request.FutureTarget import com.bumptech.glide.request.FutureTarget
import com.keylesspalace.tusky.BuildConfig.APPLICATION_ID import com.keylesspalace.tusky.BuildConfig.APPLICATION_ID
import com.keylesspalace.tusky.databinding.ActivityViewMediaBinding
import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.fragment.ViewImageFragment import com.keylesspalace.tusky.fragment.ViewImageFragment
import com.keylesspalace.tusky.pager.SingleImagePagerAdapter import com.keylesspalace.tusky.pager.SingleImagePagerAdapter
import com.keylesspalace.tusky.pager.ImagePagerAdapter import com.keylesspalace.tusky.pager.ImagePagerAdapter
import com.keylesspalace.tusky.util.getTemporaryMediaFilename import com.keylesspalace.tusky.util.getTemporaryMediaFilename
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.viewdata.AttachmentViewData import com.keylesspalace.tusky.viewdata.AttachmentViewData
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider
import com.uber.autodispose.autoDispose import com.uber.autodispose.autoDispose
import io.reactivex.Single import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
import kotlinx.android.synthetic.main.activity_view_media.*
import java.io.File import java.io.File
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.io.FileOutputStream import java.io.FileOutputStream
@ -65,27 +66,11 @@ import java.util.*
typealias ToolbarVisibilityListener = (isVisible: Boolean) -> Unit typealias ToolbarVisibilityListener = (isVisible: Boolean) -> Unit
class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener { 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 private val binding by viewBinding(ActivityViewMediaBinding::inflate)
fun newIntent(context: Context?, attachments: List<AttachmentViewData>, 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 val toolbar: View
fun newSingleImageIntent(context: Context, url: String): Intent { get() = binding.toolbar
val intent = Intent(context, ViewMediaActivity::class.java)
intent.putExtra(EXTRA_SINGLE_IMAGE_URL, url)
return intent
}
}
var isToolbarVisible = true var isToolbarVisible = true
private set private set
@ -102,7 +87,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_view_media) setContentView(binding.root)
supportPostponeEnterTransition() supportPostponeEnterTransition()
@ -125,24 +110,24 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
SingleImagePagerAdapter(this, imageUrl!!) SingleImagePagerAdapter(this, imageUrl!!)
} }
viewPager.adapter = adapter binding.viewPager.adapter = adapter
viewPager.setCurrentItem(initialPosition, false) binding.viewPager.setCurrentItem(initialPosition, false)
viewPager.registerOnPageChangeCallback(object: ViewPager2.OnPageChangeCallback() { binding.viewPager.registerOnPageChangeCallback(object: ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) { override fun onPageSelected(position: Int) {
toolbar.title = getPageTitle(position) binding.toolbar.title = getPageTitle(position)
} }
}) })
// Setup the toolbar. // Setup the toolbar.
setSupportActionBar(toolbar) setSupportActionBar(binding.toolbar)
val actionBar = supportActionBar val actionBar = supportActionBar
if (actionBar != null) { if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true) actionBar.setDisplayHomeAsUpEnabled(true)
actionBar.setDisplayShowHomeEnabled(true) actionBar.setDisplayShowHomeEnabled(true)
actionBar.title = getPageTitle(initialPosition) actionBar.title = getPageTitle(initialPosition)
} }
toolbar.setNavigationOnClickListener { supportFinishAfterTransition() } binding.toolbar.setNavigationOnClickListener { supportFinishAfterTransition() }
toolbar.setOnMenuItemClickListener { item: MenuItem -> binding.toolbar.setOnMenuItemClickListener { item: MenuItem ->
when (item.itemId) { when (item.itemId) {
R.id.action_download -> requestDownloadMedia() R.id.action_download -> requestDownloadMedia()
R.id.action_open_status -> onOpenStatus() R.id.action_open_status -> onOpenStatus()
@ -156,7 +141,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
window.statusBarColor = Color.BLACK window.statusBarColor = Color.BLACK
window.sharedElementEnterTransition.addListener(object : NoopTransitionListener { window.sharedElementEnterTransition.addListener(object : NoopTransitionListener {
override fun onTransitionEnd(transition: Transition) { override fun onTransitionEnd(transition: Transition) {
adapter.onTransitionEnd(viewPager.currentItem) adapter.onTransitionEnd(binding.viewPager.currentItem)
window.sharedElementEnterTransition.removeListener(this) window.sharedElementEnterTransition.removeListener(this)
} }
}) })
@ -165,7 +150,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.view_media_toolbar, menu) menuInflater.inflate(R.menu.view_media_toolbar, menu)
// We don't support 'open status' from single image views // 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 return true
} }
@ -192,14 +177,14 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
val alpha = if (isToolbarVisible) 1.0f else 0.0f val alpha = if (isToolbarVisible) 1.0f else 0.0f
if (isToolbarVisible) { if (isToolbarVisible) {
// If to be visible, need to make visible immediately and animate alpha // If to be visible, need to make visible immediately and animate alpha
toolbar.alpha = 0.0f binding.toolbar.alpha = 0.0f
toolbar.visibility = visibility binding.toolbar.visibility = visibility
} }
toolbar.animate().alpha(alpha) binding.toolbar.animate().alpha(alpha)
.setListener(object : AnimatorListenerAdapter() { .setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) { override fun onAnimationEnd(animation: Animator) {
toolbar.visibility = visibility binding.toolbar.visibility = visibility
animation.removeListener(this) animation.removeListener(this)
} }
}) })
@ -214,7 +199,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
} }
private fun downloadMedia() { 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 val filename = Uri.parse(url).lastPathSegment
Toast.makeText(applicationContext, resources.getString(R.string.download_image, filename), Toast.LENGTH_SHORT).show() 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) { if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
downloadMedia() downloadMedia()
} else { } 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() { private fun onOpenStatus() {
val attach = attachments!![viewPager.currentItem] val attach = attachments!![binding.viewPager.currentItem]
startActivityWithSlideInAnimation(ViewThreadActivity.startIntent(this, attach.statusId, attach.statusUrl)) startActivityWithSlideInAnimation(ViewThreadActivity.startIntent(this, attach.statusId, attach.statusUrl))
} }
private fun copyLink() { 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 val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setPrimaryClip(ClipData.newPlainText(null, url)) clipboard.setPrimaryClip(ClipData.newPlainText(null, url))
} }
@ -256,7 +241,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
if (imageUrl != null) { if (imageUrl != null) {
shareImage(directory, imageUrl!!) shareImage(directory, imageUrl!!)
} else { } else {
val attachment = attachments!![viewPager.currentItem].attachment val attachment = attachments!![binding.viewPager.currentItem].attachment
when (attachment.type) { when (attachment.type) {
Attachment.Type.IMAGE -> shareImage(directory, attachment.url) Attachment.Type.IMAGE -> shareImage(directory, attachment.url)
Attachment.Type.AUDIO, Attachment.Type.AUDIO,
@ -280,7 +265,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
private fun shareImage(directory: File, url: String) { private fun shareImage(directory: File, url: String) {
isCreating = true isCreating = true
progressBarShare.visibility = View.VISIBLE binding.progressBarShare.visibility = View.VISIBLE
invalidateOptionsMenu() invalidateOptionsMenu()
val file = File(directory, getTemporaryMediaFilename("png")) val file = File(directory, getTemporaryMediaFilename("png"))
val futureTask: FutureTarget<Bitmap> = val futureTask: FutureTarget<Bitmap> =
@ -312,14 +297,14 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
Log.d(TAG, "Download image result: $result") Log.d(TAG, "Download image result: $result")
isCreating = false isCreating = false
invalidateOptionsMenu() invalidateOptionsMenu()
progressBarShare.visibility = View.GONE binding.progressBarShare.visibility = View.GONE
if (result) if (result)
shareFile(file, "image/png") shareFile(file, "image/png")
}, },
{ error -> { error ->
isCreating = false isCreating = false
invalidateOptionsMenu() invalidateOptionsMenu()
progressBarShare.visibility = View.GONE binding.progressBarShare.visibility = View.GONE
Log.e(TAG, "Failed to download image", error) Log.e(TAG, "Failed to download image", error)
} }
) )
@ -342,6 +327,28 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
shareFile(file, mimeType) 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<AttachmentViewData>, 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) { abstract class ViewMediaAdapter(activity: FragmentActivity): FragmentStateAdapter(activity) {

View file

@ -19,60 +19,57 @@ import android.text.method.LinkMovementMethod
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import android.view.View
import android.widget.TextView
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.databinding.ItemAccountFieldBinding
import com.keylesspalace.tusky.entity.Emoji import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.entity.Field import com.keylesspalace.tusky.entity.Field
import com.keylesspalace.tusky.entity.IdentityProof import com.keylesspalace.tusky.entity.IdentityProof
import com.keylesspalace.tusky.interfaces.LinkListener import com.keylesspalace.tusky.interfaces.LinkListener
import com.keylesspalace.tusky.util.* 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<AccountFieldAdapter.ViewHolder>() { class AccountFieldAdapter(
private val linkListener: LinkListener,
private val animateEmojis: Boolean
) : RecyclerView.Adapter<BindingHolder<ItemAccountFieldBinding>>() {
var emojis: List<Emoji> = emptyList() var emojis: List<Emoji> = emptyList()
var fields: List<Either<IdentityProof, Field>> = emptyList() var fields: List<Either<IdentityProof, Field>> = emptyList()
override fun getItemCount() = fields.size override fun getItemCount() = fields.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemAccountFieldBinding> {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_account_field, parent, false) val binding = ItemAccountFieldBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(view) return BindingHolder(binding)
} }
override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: BindingHolder<ItemAccountFieldBinding>, position: Int) {
val proofOrField = fields[position] val proofOrField = fields[position]
val nameTextView = holder.binding.accountFieldName
val valueTextView = holder.binding.accountFieldValue
if(proofOrField.isLeft()) { if(proofOrField.isLeft()) {
val identityProof = proofOrField.asLeft() val identityProof = proofOrField.asLeft()
viewHolder.nameTextView.text = identityProof.provider nameTextView.text = identityProof.provider
viewHolder.valueTextView.text = LinkHelper.createClickableText(identityProof.username, identityProof.profileUrl) 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 { } else {
val field = proofOrField.asRight() val field = proofOrField.asRight()
val emojifiedName = field.name.emojify(emojis, viewHolder.nameTextView, animateEmojis) val emojifiedName = field.name.emojify(emojis, nameTextView, animateEmojis)
viewHolder.nameTextView.text = emojifiedName nameTextView.text = emojifiedName
val emojifiedValue = field.value.emojify(emojis, viewHolder.valueTextView, animateEmojis) val emojifiedValue = field.value.emojify(emojis, valueTextView, animateEmojis)
LinkHelper.setClickableText(viewHolder.valueTextView, emojifiedValue, null, linkListener) LinkHelper.setClickableText(valueTextView, emojifiedValue, null, linkListener)
if(field.verifiedAt != null) { 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 { } 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
}
} }

View file

@ -15,18 +15,16 @@
package com.keylesspalace.tusky.adapter package com.keylesspalace.tusky.adapter
import androidx.recyclerview.widget.RecyclerView
import android.text.Editable import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.EditText import androidx.recyclerview.widget.RecyclerView
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.databinding.ItemEditFieldBinding
import com.keylesspalace.tusky.entity.StringField 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<AccountFieldEditAdapter.ViewHolder>() { class AccountFieldEditAdapter : RecyclerView.Adapter<BindingHolder<ItemEditFieldBinding>>() {
private val fieldData = mutableListOf<MutableStringPair>() private val fieldData = mutableListOf<MutableStringPair>()
@ -54,20 +52,20 @@ class AccountFieldEditAdapter : RecyclerView.Adapter<AccountFieldEditAdapter.Vie
notifyItemInserted(fieldData.size - 1) notifyItemInserted(fieldData.size - 1)
} }
override fun getItemCount(): Int = fieldData.size override fun getItemCount() = fieldData.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemEditFieldBinding> {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_edit_field, parent, false) val binding = ItemEditFieldBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(view) return BindingHolder(binding)
} }
override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: BindingHolder<ItemEditFieldBinding>, position: Int) {
viewHolder.nameTextView.setText(fieldData[position].first) holder.binding.accountFieldName.setText(fieldData[position].first)
viewHolder.valueTextView.setText(fieldData[position].second) holder.binding.accountFieldValue.setText(fieldData[position].second)
viewHolder.nameTextView.addTextChangedListener(object: TextWatcher { holder.binding.accountFieldName.addTextChangedListener(object: TextWatcher {
override fun afterTextChanged(newText: Editable) { 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) {} override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
@ -75,9 +73,9 @@ class AccountFieldEditAdapter : RecyclerView.Adapter<AccountFieldEditAdapter.Vie
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
}) })
viewHolder.valueTextView.addTextChangedListener(object: TextWatcher { holder.binding.accountFieldValue.addTextChangedListener(object: TextWatcher {
override fun afterTextChanged(newText: Editable) { override fun afterTextChanged(newText: Editable) {
fieldData[viewHolder.adapterPosition].second = newText.toString() fieldData[holder.bindingAdapterPosition].second = newText.toString()
} }
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
@ -87,12 +85,6 @@ class AccountFieldEditAdapter : RecyclerView.Adapter<AccountFieldEditAdapter.Vie
} }
class ViewHolder(rootView: View) : RecyclerView.ViewHolder(rootView) {
val nameTextView: EditText = rootView.accountFieldName
val valueTextView: EditText = rootView.accountFieldValue
}
class MutableStringPair (var first: String, var second: String) class MutableStringPair (var first: String, var second: String)
} }

View file

@ -22,39 +22,35 @@ import android.view.ViewGroup
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.databinding.ItemAutocompleteAccountBinding
import com.keylesspalace.tusky.db.AccountEntity import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.* import com.keylesspalace.tusky.util.*
import kotlinx.android.synthetic.main.item_autocomplete_account.view.*
class AccountSelectionAdapter(context: Context) : ArrayAdapter<AccountEntity>(context, R.layout.item_autocomplete_account) { class AccountSelectionAdapter(context: Context) : ArrayAdapter<AccountEntity>(context, R.layout.item_autocomplete_account) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
var view = convertView
if (convertView == null) { override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater val binding = if (convertView == null) {
view = layoutInflater.inflate(R.layout.item_autocomplete_account, parent, false) ItemAutocompleteAccountBinding.inflate(LayoutInflater.from(context), parent, false)
} else {
ItemAutocompleteAccountBinding.bind(convertView)
} }
view!!
val account = getItem(position) val account = getItem(position)
if (account != null) { if (account != null) {
val username = view.username val pm = PreferenceManager.getDefaultSharedPreferences(binding.avatar.context)
val displayName = view.display_name
val avatar = view.avatar
val pm = PreferenceManager.getDefaultSharedPreferences(avatar.context)
val animateEmojis = pm.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) val animateEmojis = pm.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
username.text = account.fullName binding.username.text = account.fullName
displayName.text = account.displayName.emojify(account.emojis, displayName, animateEmojis) 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) val animateAvatar = pm.getBoolean("animateGifAvatars", false)
loadAvatar(account.profilePictureUrl, avatar, avatarRadius, animateAvatar) loadAvatar(account.profilePictureUrl, binding.avatar, avatarRadius, animateAvatar)
} }
return view return binding.root
} }
} }

View file

@ -95,7 +95,7 @@ public class BlocksAdapter extends AccountAdapter {
void setupActionListener(final AccountActionListener listener) { void setupActionListener(final AccountActionListener listener) {
unblock.setOnClickListener(v -> { unblock.setOnClickListener(v -> {
int position = getAdapterPosition(); int position = getBindingAdapterPosition();
if (position != RecyclerView.NO_POSITION) { if (position != RecyclerView.NO_POSITION) {
listener.onBlock(false, id, position); listener.onBlock(false, id, position);
} }

View file

@ -15,48 +15,44 @@
package com.keylesspalace.tusky.adapter package com.keylesspalace.tusky.adapter
import androidx.recyclerview.widget.RecyclerView
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide 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.entity.Emoji
import com.keylesspalace.tusky.util.BindingHolder
import java.util.* import java.util.*
class EmojiAdapter(emojiList: List<Emoji>, private val onEmojiSelectedListener: OnEmojiSelectedListener) : RecyclerView.Adapter<EmojiAdapter.EmojiHolder>() { class EmojiAdapter(
private val emojiList : List<Emoji> emojiList: List<Emoji>,
private val onEmojiSelectedListener: OnEmojiSelectedListener
) : RecyclerView.Adapter<BindingHolder<ItemEmojiButtonBinding>>() {
init { private val emojiList : List<Emoji> = emojiList.filter { emoji -> emoji.visibleInPicker == null || emoji.visibleInPicker }
this.emojiList = emojiList.filter { emoji -> emoji.visibleInPicker == null || emoji.visibleInPicker } .sortedBy { it.shortcode.toLowerCase(Locale.ROOT) }
.sortedBy { it.shortcode.toLowerCase(Locale.ROOT) }
override fun getItemCount() = emojiList.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemEmojiButtonBinding> {
val binding = ItemEmojiButtonBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return BindingHolder(binding)
} }
override fun getItemCount(): Int { override fun onBindViewHolder(holder: BindingHolder<ItemEmojiButtonBinding>, position: 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) {
val emoji = emojiList[position] val emoji = emojiList[position]
val emojiImageView = holder.binding.root
Glide.with(viewHolder.emojiImageView) Glide.with(emojiImageView)
.load(emoji.url) .load(emoji.url)
.into(viewHolder.emojiImageView) .into(emojiImageView)
viewHolder.emojiImageView.setOnClickListener { emojiImageView.setOnClickListener {
onEmojiSelectedListener.onEmojiSelected(emoji.shortcode) onEmojiSelectedListener.onEmojiSelected(emoji.shortcode)
} }
viewHolder.emojiImageView.contentDescription = emoji.shortcode emojiImageView.contentDescription = emoji.shortcode
} }
class EmojiHolder(val emojiImageView: ImageView) : RecyclerView.ViewHolder(emojiImageView)
} }
interface OnEmojiSelectedListener { interface OnEmojiSelectedListener {

View file

@ -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 <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.adapter package com.keylesspalace.tusky.adapter
import android.graphics.Typeface import android.graphics.Typeface
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.text.Spanned import android.text.Spanned
import android.text.style.StyleSpan import android.text.style.StyleSpan
import android.view.View
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding
import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.interfaces.AccountActionListener import com.keylesspalace.tusky.interfaces.AccountActionListener
import com.keylesspalace.tusky.util.* import com.keylesspalace.tusky.util.*
import kotlinx.android.synthetic.main.item_follow_request_notification.view.*
internal class FollowRequestViewHolder( class FollowRequestViewHolder(
itemView: View, private val binding: ItemFollowRequestBinding,
private val showHeader: Boolean) : RecyclerView.ViewHolder(itemView) { private val showHeader: Boolean
private var id: String? = null ) : RecyclerView.ViewHolder(binding.root) {
fun setupWithAccount(account: Account, animateAvatar: Boolean, animateEmojis: Boolean) { fun setupWithAccount(account: Account, animateAvatar: Boolean, animateEmojis: Boolean) {
id = account.id
val wrappedName = account.name.unicodeWrap() val wrappedName = account.name.unicodeWrap()
val emojifiedName: CharSequence = wrappedName.emojify(account.emojis, itemView, animateEmojis) val emojifiedName: CharSequence = wrappedName.emojify(account.emojis, itemView, animateEmojis)
itemView.displayNameTextView.text = emojifiedName binding.displayNameTextView.text = emojifiedName
if (showHeader) { if (showHeader) {
val wholeMessage: String = itemView.context.getString(R.string.notification_follow_request_format, wrappedName) 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) setSpan(StyleSpan(Typeface.BOLD), 0, wrappedName.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}.emojify(account.emojis, itemView, animateEmojis) }.emojify(account.emojis, itemView, animateEmojis)
} }
itemView.notificationTextView?.visible(showHeader) binding.notificationTextView.visible(showHeader)
val format = itemView.context.getString(R.string.status_username_format) val format = itemView.context.getString(R.string.status_username_format)
val formattedUsername = String.format(format, account.username) val formattedUsername = String.format(format, account.username)
itemView.usernameTextView.text = formattedUsername binding.usernameTextView.text = formattedUsername
val avatarRadius = itemView.avatar.context.resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp) val avatarRadius = binding.avatar.context.resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp)
loadAvatar(account.avatar, itemView.avatar, avatarRadius, animateAvatar) loadAvatar(account.avatar, binding.avatar, avatarRadius, animateAvatar)
} }
fun setupActionListener(listener: AccountActionListener) { fun setupActionListener(listener: AccountActionListener, accountId: String) {
itemView.acceptButton.setOnClickListener { binding.acceptButton.setOnClickListener {
val position = adapterPosition val position = bindingAdapterPosition
if (position != RecyclerView.NO_POSITION) { if (position != RecyclerView.NO_POSITION) {
listener.onRespondToFollowRequest(true, id, position) listener.onRespondToFollowRequest(true, accountId, position)
} }
} }
itemView.rejectButton.setOnClickListener { binding.rejectButton.setOnClickListener {
val position = adapterPosition val position = bindingAdapterPosition
if (position != RecyclerView.NO_POSITION) { 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) }
} }
} }

View file

@ -23,6 +23,7 @@ import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding;
import com.keylesspalace.tusky.interfaces.AccountActionListener; import com.keylesspalace.tusky.interfaces.AccountActionListener;
public class FollowRequestsAdapter extends AccountAdapter { public class FollowRequestsAdapter extends AccountAdapter {
@ -37,9 +38,8 @@ public class FollowRequestsAdapter extends AccountAdapter {
switch (viewType) { switch (viewType) {
default: default:
case VIEW_TYPE_ACCOUNT: { case VIEW_TYPE_ACCOUNT: {
View view = LayoutInflater.from(parent.getContext()) ItemFollowRequestBinding binding = ItemFollowRequestBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
.inflate(R.layout.item_follow_request, parent, false); return new FollowRequestViewHolder(binding, false);
return new FollowRequestViewHolder(view, false);
} }
case VIEW_TYPE_FOOTER: { case VIEW_TYPE_FOOTER: {
View view = LayoutInflater.from(parent.getContext()) View view = LayoutInflater.from(parent.getContext())
@ -54,7 +54,7 @@ public class FollowRequestsAdapter extends AccountAdapter {
if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) { if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) {
FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder; FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder;
holder.setupWithAccount(accountList.get(position), animateAvatar, animateEmojis); holder.setupWithAccount(accountList.get(position), animateAvatar, animateEmojis);
holder.setupActionListener(accountActionListener); holder.setupActionListener(accountActionListener, accountList.get(position).getId());
} }
} }
} }

View file

@ -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 <http://www.gnu.org/licenses>. */
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<HeaderViewHolder>() {
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)

View file

@ -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) }
}
}

View file

@ -21,21 +21,22 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.databinding.ItemPickerListBinding
import com.keylesspalace.tusky.entity.MastoList import com.keylesspalace.tusky.entity.MastoList
import kotlinx.android.synthetic.main.item_picker_list.view.*
class ListSelectionAdapter(context: Context) : ArrayAdapter<MastoList>(context, R.layout.item_autocomplete_hashtag) { class ListSelectionAdapter(context: Context) : ArrayAdapter<MastoList>(context, R.layout.item_picker_list) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater val binding = if (convertView == null) {
ItemPickerListBinding.inflate(LayoutInflater.from(context), parent, false)
val view = convertView } else {
?: layoutInflater.inflate(R.layout.item_picker_list, parent, false) ItemPickerListBinding.bind(convertView)
getItem(position)?.let { list ->
view.title.text = list.title
} }
return view getItem(position)?.let { list ->
binding.root.text = list.title
}
return binding.root
} }
} }

View file

@ -9,7 +9,6 @@ import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.core.view.ViewCompat; import androidx.core.view.ViewCompat;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.R;
@ -123,9 +122,9 @@ public class MutesAdapter extends AccountAdapter {
} }
void setupActionListener(final AccountActionListener listener) { 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( muteNotifications.setOnClickListener(
v -> listener.onMute(true, id, getAdapterPosition(), !notifications)); v -> listener.onMute(true, id, getBindingAdapterPosition(), !notifications));
itemView.setOnClickListener(v -> listener.onViewAccount(id)); itemView.setOnClickListener(v -> listener.onViewAccount(id));
} }
} }

View file

@ -16,29 +16,28 @@
package com.keylesspalace.tusky.adapter package com.keylesspalace.tusky.adapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import com.keylesspalace.tusky.databinding.ItemNetworkStateBinding
import com.keylesspalace.tusky.util.NetworkState import com.keylesspalace.tusky.util.NetworkState
import com.keylesspalace.tusky.util.Status import com.keylesspalace.tusky.util.Status
import com.keylesspalace.tusky.util.visible 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) private val retryCallback: () -> Unit)
: RecyclerView.ViewHolder(itemView) { : RecyclerView.ViewHolder(binding.root) {
fun setUpWithNetworkState(state: NetworkState?, fullScreen: Boolean) { fun setUpWithNetworkState(state: NetworkState?, fullScreen: Boolean) {
itemView.progressBar.visible(state?.status == Status.RUNNING) binding.progressBar.visible(state?.status == Status.RUNNING)
itemView.retryButton.visible(state?.status == Status.FAILED) binding.retryButton.visible(state?.status == Status.FAILED)
itemView.errorMsg.visible(state?.msg != null) binding.errorMsg.visible(state?.msg != null)
itemView.errorMsg.text = state?.msg binding.errorMsg.text = state?.msg
itemView.retryButton.setOnClickListener { binding.retryButton.setOnClickListener {
retryCallback() retryCallback()
} }
if(fullScreen) { if(fullScreen) {
itemView.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT binding.root.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
} else { } else {
itemView.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT binding.root.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT
} }
} }

View file

@ -39,6 +39,7 @@ import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide; import com.bumptech.glide.Glide;
import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding;
import com.keylesspalace.tusky.entity.Account; import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.entity.Emoji; import com.keylesspalace.tusky.entity.Emoji;
import com.keylesspalace.tusky.entity.Notification; import com.keylesspalace.tusky.entity.Notification;
@ -125,9 +126,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
return new FollowViewHolder(view, statusDisplayOptions); return new FollowViewHolder(view, statusDisplayOptions);
} }
case VIEW_TYPE_FOLLOW_REQUEST: { case VIEW_TYPE_FOLLOW_REQUEST: {
View view = inflater ItemFollowRequestBinding binding = ItemFollowRequestBinding.inflate(inflater, parent, false);
.inflate(R.layout.item_follow_request_notification, parent, false); return new FollowRequestViewHolder(binding, true);
return new FollowRequestViewHolder(view, true);
} }
case VIEW_TYPE_PLACEHOLDER: { case VIEW_TYPE_PLACEHOLDER: {
View view = inflater View view = inflater
@ -233,8 +233,9 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
if (payloadForHolder == null) { if (payloadForHolder == null) {
FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder; FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder;
holder.setupWithAccount(concreteNotificaton.getAccount(), statusDisplayOptions.animateAvatars(), statusDisplayOptions.animateEmojis()); holder.setupWithAccount(concreteNotificaton.getAccount(), statusDisplayOptions.animateAvatars(), statusDisplayOptions.animateEmojis());
holder.setupActionListener(accountActionListener); holder.setupActionListener(accountActionListener, concreteNotificaton.getAccount().getId());
} }
break;
} }
default: default:
} }
@ -540,8 +541,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
} }
contentWarningButton.setOnClickListener(view -> { contentWarningButton.setOnClickListener(view -> {
if (getAdapterPosition() != RecyclerView.NO_POSITION) { if (getBindingAdapterPosition() != RecyclerView.NO_POSITION) {
notificationActionListener.onExpandedChange(!statusViewData.isExpanded(), getAdapterPosition()); notificationActionListener.onExpandedChange(!statusViewData.isExpanded(), getBindingAdapterPosition());
} }
statusContent.setVisibility(statusViewData.isExpanded() ? View.GONE : View.VISIBLE); statusContent.setVisibility(statusViewData.isExpanded() ? View.GONE : View.VISIBLE);
}); });
@ -618,7 +619,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
if (statusViewData.isCollapsible() && (statusViewData.isExpanded() || !hasSpoiler)) { if (statusViewData.isCollapsible() && (statusViewData.isExpanded() || !hasSpoiler)) {
contentCollapseButton.setOnClickListener(view -> { contentCollapseButton.setOnClickListener(view -> {
int position = getAdapterPosition(); int position = getBindingAdapterPosition();
if (position != RecyclerView.NO_POSITION && notificationActionListener != null) { if (position != RecyclerView.NO_POSITION && notificationActionListener != null) {
notificationActionListener.onNotificationContentCollapsedChange(!statusViewData.isCollapsed(), position); notificationActionListener.onNotificationContentCollapsedChange(!statusViewData.isCollapsed(), position);
} }

View file

@ -41,7 +41,7 @@ public final class PlaceholderViewHolder extends RecyclerView.ViewHolder {
loadMoreButton.setEnabled(true); loadMoreButton.setEnabled(true);
loadMoreButton.setOnClickListener(v -> { loadMoreButton.setOnClickListener(v -> {
loadMoreButton.setEnabled(false); loadMoreButton.setEnabled(false);
listener.onLoadMore(getAdapterPosition()); listener.onLoadMore(getBindingAdapterPosition());
}); });
} }

View file

@ -18,19 +18,18 @@ package com.keylesspalace.tusky.adapter
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.CheckBox
import android.widget.RadioButton
import android.widget.TextView
import androidx.emoji.text.EmojiCompat import androidx.emoji.text.EmojiCompat
import androidx.recyclerview.widget.RecyclerView 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.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.PollOptionViewData
import com.keylesspalace.tusky.viewdata.buildDescription import com.keylesspalace.tusky.viewdata.buildDescription
import com.keylesspalace.tusky.viewdata.calculatePercent import com.keylesspalace.tusky.viewdata.calculatePercent
class PollAdapter: RecyclerView.Adapter<PollViewHolder>() { class PollAdapter: RecyclerView.Adapter<BindingHolder<ItemPollBinding>>() {
private var pollOptions: List<PollOptionViewData> = emptyList() private var pollOptions: List<PollOptionViewData> = emptyList()
private var voteCount: Int = 0 private var voteCount: Int = 0
@ -64,51 +63,54 @@ class PollAdapter: RecyclerView.Adapter<PollViewHolder>() {
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PollViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemPollBinding> {
return PollViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_poll, parent, false)) val binding = ItemPollBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return BindingHolder(binding)
} }
override fun getItemCount(): Int { override fun getItemCount() = pollOptions.size
return pollOptions.size
}
override fun onBindViewHolder(holder: PollViewHolder, position: Int) { override fun onBindViewHolder(holder: BindingHolder<ItemPollBinding>, position: Int) {
val option = pollOptions[position] val option = pollOptions[position]
holder.resultTextView.visible(mode == RESULT) val resultTextView = holder.binding.statusPollOptionResult
holder.radioButton.visible(mode == SINGLE) val radioButton = holder.binding.statusPollRadioButton
holder.checkBox.visible(mode == MULTIPLE) val checkBox = holder.binding.statusPollCheckbox
resultTextView.visible(mode == RESULT)
radioButton.visible(mode == SINGLE)
checkBox.visible(mode == MULTIPLE)
when(mode) { when(mode) {
RESULT -> { RESULT -> {
val percent = calculatePercent(option.votesCount, votersCount, voteCount) val percent = calculatePercent(option.votesCount, votersCount, voteCount)
val emojifiedPollOptionText = buildDescription(option.title, percent, holder.resultTextView.context) val emojifiedPollOptionText = buildDescription(option.title, percent, resultTextView.context)
.emojify(emojis, holder.resultTextView, animateEmojis) .emojify(emojis, resultTextView, animateEmojis)
holder.resultTextView.text = EmojiCompat.get().process(emojifiedPollOptionText) resultTextView.text = EmojiCompat.get().process(emojifiedPollOptionText)
val level = percent * 100 val level = percent * 100
holder.resultTextView.background.level = level resultTextView.background.level = level
holder.resultTextView.setOnClickListener(resultClickListener) resultTextView.setOnClickListener(resultClickListener)
} }
SINGLE -> { SINGLE -> {
val emojifiedPollOptionText = option.title.emojify(emojis, holder.radioButton, animateEmojis) val emojifiedPollOptionText = option.title.emojify(emojis, radioButton, animateEmojis)
holder.radioButton.text = EmojiCompat.get().process(emojifiedPollOptionText) radioButton.text = EmojiCompat.get().process(emojifiedPollOptionText)
holder.radioButton.isChecked = option.selected radioButton.isChecked = option.selected
holder.radioButton.setOnClickListener { radioButton.setOnClickListener {
pollOptions.forEachIndexed { index, pollOption -> pollOptions.forEachIndexed { index, pollOption ->
pollOption.selected = index == holder.adapterPosition pollOption.selected = index == holder.bindingAdapterPosition
notifyItemChanged(index) notifyItemChanged(index)
} }
} }
} }
MULTIPLE -> { MULTIPLE -> {
val emojifiedPollOptionText = option.title.emojify(emojis, holder.checkBox, animateEmojis) val emojifiedPollOptionText = option.title.emojify(emojis, checkBox, animateEmojis)
holder.checkBox.text = EmojiCompat.get().process(emojifiedPollOptionText) checkBox.text = EmojiCompat.get().process(emojifiedPollOptionText)
holder.checkBox.isChecked = option.selected checkBox.isChecked = option.selected
holder.checkBox.setOnCheckedChangeListener { _, isChecked -> checkBox.setOnCheckedChangeListener { _, isChecked ->
pollOptions[holder.adapterPosition].selected = isChecked pollOptions[holder.bindingAdapterPosition].selected = isChecked
} }
} }
} }
@ -121,13 +123,3 @@ class PollAdapter: RecyclerView.Adapter<PollViewHolder>() {
const val MULTIPLE = 2 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)
}

View file

@ -63,5 +63,4 @@ class PreviewPollOptionsAdapter: RecyclerView.Adapter<PreviewViewHolder>() {
} }
class PreviewViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) class PreviewViewHolder(itemView: View): RecyclerView.ViewHolder(itemView)

View file

@ -113,9 +113,9 @@ public class SavedTootAdapter extends RecyclerView.Adapter {
suppr.setOnClickListener(v -> { suppr.setOnClickListener(v -> {
v.setEnabled(false); 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));
} }
} }
} }

View file

@ -217,8 +217,8 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
setContentWarningButtonText(expanded); setContentWarningButtonText(expanded);
contentWarningButton.setOnClickListener(view -> { contentWarningButton.setOnClickListener(view -> {
contentWarningDescription.invalidate(); contentWarningDescription.invalidate();
if (getAdapterPosition() != RecyclerView.NO_POSITION) { if (getBindingAdapterPosition() != RecyclerView.NO_POSITION) {
listener.onExpandedChange(!expanded, getAdapterPosition()); listener.onExpandedChange(!expanded, getBindingAdapterPosition());
} }
setContentWarningButtonText(!expanded); setContentWarningButtonText(!expanded);
@ -513,15 +513,15 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
sensitiveMediaWarning.setVisibility(showingContent ? View.GONE : View.VISIBLE); sensitiveMediaWarning.setVisibility(showingContent ? View.GONE : View.VISIBLE);
sensitiveMediaShow.setVisibility(showingContent ? View.VISIBLE : View.GONE); sensitiveMediaShow.setVisibility(showingContent ? View.VISIBLE : View.GONE);
sensitiveMediaShow.setOnClickListener(v -> { sensitiveMediaShow.setOnClickListener(v -> {
if (getAdapterPosition() != RecyclerView.NO_POSITION) { if (getBindingAdapterPosition() != RecyclerView.NO_POSITION) {
listener.onContentHiddenChange(false, getAdapterPosition()); listener.onContentHiddenChange(false, getBindingAdapterPosition());
} }
v.setVisibility(View.GONE); v.setVisibility(View.GONE);
sensitiveMediaWarning.setVisibility(View.VISIBLE); sensitiveMediaWarning.setVisibility(View.VISIBLE);
}); });
sensitiveMediaWarning.setOnClickListener(v -> { sensitiveMediaWarning.setOnClickListener(v -> {
if (getAdapterPosition() != RecyclerView.NO_POSITION) { if (getBindingAdapterPosition() != RecyclerView.NO_POSITION) {
listener.onContentHiddenChange(true, getAdapterPosition()); listener.onContentHiddenChange(true, getBindingAdapterPosition());
} }
v.setVisibility(View.GONE); v.setVisibility(View.GONE);
sensitiveMediaShow.setVisibility(View.VISIBLE); sensitiveMediaShow.setVisibility(View.VISIBLE);
@ -582,10 +582,10 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
private void setAttachmentClickListener(View view, StatusActionListener listener, private void setAttachmentClickListener(View view, StatusActionListener listener,
int index, Attachment attachment, boolean animateTransition) { int index, Attachment attachment, boolean animateTransition) {
view.setOnClickListener(v -> { view.setOnClickListener(v -> {
int position = getAdapterPosition(); int position = getBindingAdapterPosition();
if (position != RecyclerView.NO_POSITION) { if (position != RecyclerView.NO_POSITION) {
if (sensitiveMediaWarning.getVisibility() == View.VISIBLE) { if (sensitiveMediaWarning.getVisibility() == View.VISIBLE) {
listener.onContentHiddenChange(true, getAdapterPosition()); listener.onContentHiddenChange(true, getBindingAdapterPosition());
} else { } else {
listener.onViewMedia(position, index, animateTransition ? v : null); listener.onViewMedia(position, index, animateTransition ? v : null);
} }
@ -627,7 +627,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
displayName.setOnClickListener(profileButtonClickListener); displayName.setOnClickListener(profileButtonClickListener);
replyButton.setOnClickListener(v -> { replyButton.setOnClickListener(v -> {
int position = getAdapterPosition(); int position = getBindingAdapterPosition();
if (position != RecyclerView.NO_POSITION) { if (position != RecyclerView.NO_POSITION) {
listener.onReply(position); listener.onReply(position);
} }
@ -635,7 +635,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
if (reblogButton != null) { if (reblogButton != null) {
reblogButton.setEventListener((button, buttonState) -> { reblogButton.setEventListener((button, buttonState) -> {
// return true to play animaion // return true to play animaion
int position = getAdapterPosition(); int position = getBindingAdapterPosition();
if (position != RecyclerView.NO_POSITION) { if (position != RecyclerView.NO_POSITION) {
if (statusDisplayOptions.confirmReblogs()) { if (statusDisplayOptions.confirmReblogs()) {
showConfirmReblogDialog(listener, statusContent, buttonState, position); showConfirmReblogDialog(listener, statusContent, buttonState, position);
@ -651,7 +651,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
} }
favouriteButton.setEventListener((button, buttonState) -> { favouriteButton.setEventListener((button, buttonState) -> {
int position = getAdapterPosition(); int position = getBindingAdapterPosition();
if (position != RecyclerView.NO_POSITION) { if (position != RecyclerView.NO_POSITION) {
listener.onFavourite(!buttonState, position); listener.onFavourite(!buttonState, position);
} }
@ -659,7 +659,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
}); });
bookmarkButton.setEventListener((button, buttonState) -> { bookmarkButton.setEventListener((button, buttonState) -> {
int position = getAdapterPosition(); int position = getBindingAdapterPosition();
if (position != RecyclerView.NO_POSITION) { if (position != RecyclerView.NO_POSITION) {
listener.onBookmark(!buttonState, position); listener.onBookmark(!buttonState, position);
} }
@ -667,7 +667,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
}); });
moreButton.setOnClickListener(v -> { moreButton.setOnClickListener(v -> {
int position = getAdapterPosition(); int position = getBindingAdapterPosition();
if (position != RecyclerView.NO_POSITION) { if (position != RecyclerView.NO_POSITION) {
listener.onMore(v, 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 * just eat the clicks instead of deferring to the parent listener, but WILL respond to a
* listener directly on the TextView, for whatever reason. */ * listener directly on the TextView, for whatever reason. */
View.OnClickListener viewThreadListener = v -> { View.OnClickListener viewThreadListener = v -> {
int position = getAdapterPosition(); int position = getBindingAdapterPosition();
if (position != RecyclerView.NO_POSITION) { if (position != RecyclerView.NO_POSITION) {
listener.onViewThread(position); listener.onViewThread(position);
} }
@ -926,7 +926,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
if (expired || poll.getVoted()) { if (expired || poll.getVoted()) {
// no voting possible // no voting possible
View.OnClickListener viewThreadListener = v -> { View.OnClickListener viewThreadListener = v -> {
int position = getAdapterPosition(); int position = getBindingAdapterPosition();
if (position != RecyclerView.NO_POSITION) { if (position != RecyclerView.NO_POSITION) {
listener.onViewThread(position); listener.onViewThread(position);
} }
@ -958,7 +958,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
pollButton.setOnClickListener(v -> { pollButton.setOnClickListener(v -> {
int position = getAdapterPosition(); int position = getBindingAdapterPosition();
if (position != RecyclerView.NO_POSITION) { if (position != RecyclerView.NO_POSITION) {

View file

@ -72,13 +72,13 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
} }
reblogs.setOnClickListener(v -> { reblogs.setOnClickListener(v -> {
int position = getAdapterPosition(); int position = getBindingAdapterPosition();
if (position != RecyclerView.NO_POSITION) { if (position != RecyclerView.NO_POSITION) {
listener.onShowReblogs(position); listener.onShowReblogs(position);
} }
}); });
favourites.setOnClickListener(v -> { favourites.setOnClickListener(v -> {
int position = getAdapterPosition(); int position = getBindingAdapterPosition();
if (position != RecyclerView.NO_POSITION) { if (position != RecyclerView.NO_POSITION) {
listener.onShowFavs(position); listener.onShowFavs(position);
} }

View file

@ -67,7 +67,7 @@ public class StatusViewHolder extends StatusBaseViewHolder {
hideStatusInfo(); hideStatusInfo();
} else { } else {
setRebloggedByDisplayName(rebloggedByDisplayName, status, statusDisplayOptions); 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 */ /* input filter for TextViews have to be set before text */
if (status.isCollapsible() && (status.isExpanded() || TextUtils.isEmpty(status.getSpoilerText()))) { if (status.isCollapsible() && (status.isExpanded() || TextUtils.isEmpty(status.getSpoilerText()))) {
contentCollapseButton.setOnClickListener(view -> { contentCollapseButton.setOnClickListener(view -> {
int position = getAdapterPosition(); int position = getBindingAdapterPosition();
if (position != RecyclerView.NO_POSITION) if (position != RecyclerView.NO_POSITION)
listener.onContentCollapsedChange(!status.isCollapsed(), position); listener.onContentCollapsedChange(!status.isCollapsed(), position);
}); });

View file

@ -18,19 +18,21 @@ package com.keylesspalace.tusky.adapter
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.size import androidx.core.view.size
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.keylesspalace.tusky.HASHTAG import com.keylesspalace.tusky.HASHTAG
import com.keylesspalace.tusky.LIST import com.keylesspalace.tusky.LIST
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.TabData 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.ThemeUtils
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.show
import kotlinx.android.synthetic.main.item_tab_preference.view.*
interface ItemInteractionListener { interface ItemInteractionListener {
fun onTabAdded(tab: TabData) fun onTabAdded(tab: TabData)
@ -44,61 +46,69 @@ interface ItemInteractionListener {
class TabAdapter(private var data: List<TabData>, class TabAdapter(private var data: List<TabData>,
private val small: Boolean, private val small: Boolean,
private val listener: ItemInteractionListener, private val listener: ItemInteractionListener,
private var removeButtonEnabled: Boolean = false) : RecyclerView.Adapter<TabAdapter.ViewHolder>() { private var removeButtonEnabled: Boolean = false
) : RecyclerView.Adapter<BindingHolder<ViewBinding>>() {
fun updateData(newData: List<TabData>) { fun updateData(newData: List<TabData>) {
this.data = newData this.data = newData
notifyDataSetChanged() notifyDataSetChanged()
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ViewBinding> {
val layoutId = if (small) { val binding = if (small) {
R.layout.item_tab_preference_small ItemTabPreferenceSmallBinding.inflate(LayoutInflater.from(parent.context), parent, false)
} else { } 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 BindingHolder(binding)
return ViewHolder(view)
} }
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: BindingHolder<ViewBinding>, position: Int) {
val context = holder.itemView.context val context = holder.itemView.context
val tab = data[position] 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) { 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) listener.onTabAdded(tab)
} }
}
holder.itemView.imageView?.setOnTouchListener { _, event -> } else {
if (event.action == MotionEvent.ACTION_DOWN) { val binding = holder.binding as ItemTabPreferenceBinding
listener.onStartDrag(holder)
true if (tab.id == LIST) {
binding.textView.text = tab.arguments.getOrNull(1).orEmpty()
} else { } else {
false binding.textView.setText(tab.text)
} }
}
holder.itemView.removeButton?.setOnClickListener { binding.textView.setCompoundDrawablesRelativeWithIntrinsicBounds(tab.icon, 0, 0, 0)
listener.onTabRemoved(holder.adapterPosition)
} binding.imageView.setOnTouchListener { _, event ->
if (holder.itemView.removeButton != null) { if (event.action == MotionEvent.ACTION_DOWN) {
holder.itemView.removeButton.isEnabled = removeButtonEnabled listener.onStartDrag(holder)
true
} else {
false
}
}
binding.removeButton.setOnClickListener {
listener.onTabRemoved(holder.bindingAdapterPosition)
}
binding.removeButton.isEnabled = removeButtonEnabled
ThemeUtils.setDrawableTint( ThemeUtils.setDrawableTint(
holder.itemView.context, holder.itemView.context,
holder.itemView.removeButton.drawable, binding.removeButton.drawable,
(if (removeButtonEnabled) android.R.attr.textColorTertiary else R.attr.textColorDisabled) (if (removeButtonEnabled) android.R.attr.textColorTertiary else R.attr.textColorDisabled)
) )
}
if (!small) {
if (tab.id == HASHTAG) { 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). * 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<TabData>,
*/ */
tab.arguments.forEachIndexed { i, arg -> 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 { ?: 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)) chipIconTint = ColorStateList.valueOf(ThemeUtils.getColor(context, android.R.attr.textColorPrimary))
} }
@ -121,21 +131,21 @@ class TabAdapter(private var data: List<TabData>,
} else { } else {
chip.setChipIconResource(R.drawable.ic_cancel_24dp) chip.setChipIconResource(R.drawable.ic_cancel_24dp)
chip.setOnClickListener { chip.setOnClickListener {
listener.onChipClicked(tab, holder.adapterPosition, i) listener.onChipClicked(tab, holder.bindingAdapterPosition, i)
} }
} }
} }
while(holder.itemView.chipGroup.size - 1 > tab.arguments.size) { while(binding.chipGroup.size - 1 > tab.arguments.size) {
holder.itemView.chipGroup.removeViewAt(tab.arguments.size) binding.chipGroup.removeViewAt(tab.arguments.size)
} }
holder.itemView.actionChip.setOnClickListener { binding.actionChip.setOnClickListener {
listener.onActionChipClicked(tab, holder.adapterPosition) listener.onActionChipClicked(tab, holder.bindingAdapterPosition)
} }
} else { } else {
holder.itemView.chipGroup.hide() binding.chipGroup.hide()
} }
} }
} }
@ -148,6 +158,4 @@ class TabAdapter(private var data: List<TabData>,
notifyDataSetChanged() notifyDataSetChanged()
} }
} }
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
} }

View file

@ -19,19 +19,17 @@ import android.view.ContextThemeWrapper
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView
import androidx.core.view.size import androidx.core.view.size
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.databinding.ItemAnnouncementBinding
import com.keylesspalace.tusky.entity.Announcement import com.keylesspalace.tusky.entity.Announcement
import com.keylesspalace.tusky.entity.Emoji import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.interfaces.LinkListener import com.keylesspalace.tusky.interfaces.LinkListener
import com.keylesspalace.tusky.util.BindingHolder
import com.keylesspalace.tusky.util.LinkHelper import com.keylesspalace.tusky.util.LinkHelper
import com.keylesspalace.tusky.util.emojify import com.keylesspalace.tusky.util.emojify
import kotlinx.android.synthetic.main.item_announcement.view.*
interface AnnouncementActionListener: LinkListener { interface AnnouncementActionListener: LinkListener {
fun openReactionPicker(announcementId: String, target: View) fun openReactionPicker(announcementId: String, target: View)
@ -44,16 +42,74 @@ class AnnouncementAdapter(
private val listener: AnnouncementActionListener, private val listener: AnnouncementActionListener,
private val wellbeingEnabled: Boolean = false, private val wellbeingEnabled: Boolean = false,
private val animateEmojis: Boolean = false private val animateEmojis: Boolean = false
) : RecyclerView.Adapter<AnnouncementAdapter.AnnouncementViewHolder>() { ) : RecyclerView.Adapter<BindingHolder<ItemAnnouncementBinding>>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AnnouncementViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemAnnouncementBinding> {
val view = LayoutInflater.from(parent.context) val binding = ItemAnnouncementBinding.inflate(LayoutInflater.from(parent.context), parent, false)
.inflate(R.layout.item_announcement, parent, false) return BindingHolder(binding)
return AnnouncementViewHolder(view)
} }
override fun onBindViewHolder(viewHolder: AnnouncementViewHolder, position: Int) { override fun onBindViewHolder(holder: BindingHolder<ItemAnnouncementBinding>, position: Int) {
viewHolder.bind(items[position]) 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 override fun getItemCount() = items.size
@ -62,67 +118,4 @@ class AnnouncementAdapter(
this.items = items this.items = items
notifyDataSetChanged() 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)
}
}
}
} }

View file

@ -30,13 +30,12 @@ import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.ViewTagActivity import com.keylesspalace.tusky.ViewTagActivity
import com.keylesspalace.tusky.adapter.EmojiAdapter import com.keylesspalace.tusky.adapter.EmojiAdapter
import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener
import com.keylesspalace.tusky.databinding.ActivityAnnouncementsBinding
import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.* import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.view.EmojiPicker import com.keylesspalace.tusky.view.EmojiPicker
import kotlinx.android.synthetic.main.activity_announcements.*
import kotlinx.android.synthetic.main.toolbar_basic.*
import javax.inject.Inject import javax.inject.Inject
class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener, OnEmojiSelectedListener, Injectable { class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener, OnEmojiSelectedListener, Injectable {
@ -46,6 +45,8 @@ class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener,
private val viewModel: AnnouncementsViewModel by viewModels { viewModelFactory } private val viewModel: AnnouncementsViewModel by viewModels { viewModelFactory }
private val binding by viewBinding(ActivityAnnouncementsBinding::inflate)
private lateinit var adapter: AnnouncementAdapter private lateinit var adapter: AnnouncementAdapter
private val picker by lazy { EmojiPicker(this) } private val picker by lazy { EmojiPicker(this) }
@ -63,22 +64,22 @@ class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener,
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_announcements) setContentView(binding.root)
setSupportActionBar(toolbar) setSupportActionBar(binding.includedToolbar.toolbar)
supportActionBar?.apply { supportActionBar?.apply {
title = getString(R.string.title_announcements) title = getString(R.string.title_announcements)
setDisplayHomeAsUpEnabled(true) setDisplayHomeAsUpEnabled(true)
setDisplayShowHomeEnabled(true) setDisplayShowHomeEnabled(true)
} }
swipeRefreshLayout.setOnRefreshListener(this::refreshAnnouncements) binding.swipeRefreshLayout.setOnRefreshListener(this::refreshAnnouncements)
swipeRefreshLayout.setColorSchemeResources(R.color.chinwag_green) binding.swipeRefreshLayout.setColorSchemeResources(R.color.chinwag_green)
announcementsList.setHasFixedSize(true) binding.announcementsList.setHasFixedSize(true)
announcementsList.layoutManager = LinearLayoutManager(this) binding.announcementsList.layoutManager = LinearLayoutManager(this)
val divider = DividerItemDecoration(this, DividerItemDecoration.VERTICAL) val divider = DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
announcementsList.addItemDecoration(divider) binding.announcementsList.addItemDecoration(divider)
val preferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) val preferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
val wellbeingEnabled = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false) val wellbeingEnabled = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false)
@ -86,31 +87,31 @@ class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener,
adapter = AnnouncementAdapter(emptyList(), this, wellbeingEnabled, animateEmojis) adapter = AnnouncementAdapter(emptyList(), this, wellbeingEnabled, animateEmojis)
announcementsList.adapter = adapter binding.announcementsList.adapter = adapter
viewModel.announcements.observe(this) { viewModel.announcements.observe(this) {
when (it) { when (it) {
is Success -> { is Success -> {
progressBar.hide() binding.progressBar.hide()
swipeRefreshLayout.isRefreshing = false binding.swipeRefreshLayout.isRefreshing = false
if (it.data.isNullOrEmpty()) { if (it.data.isNullOrEmpty()) {
errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_announcements) binding.errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_announcements)
errorMessageView.show() binding.errorMessageView.show()
} else { } else {
errorMessageView.hide() binding.errorMessageView.hide()
} }
adapter.updateList(it.data ?: listOf()) adapter.updateList(it.data ?: listOf())
} }
is Loading -> { is Loading -> {
errorMessageView.hide() binding.errorMessageView.hide()
} }
is Error -> { is Error -> {
progressBar.hide() binding.progressBar.hide()
swipeRefreshLayout.isRefreshing = false binding.swipeRefreshLayout.isRefreshing = false
errorMessageView.setup(R.drawable.elephant_error, R.string.error_generic) { binding.errorMessageView.setup(R.drawable.elephant_error, R.string.error_generic) {
refreshAnnouncements() refreshAnnouncements()
} }
errorMessageView.show() binding.errorMessageView.show()
} }
} }
} }
@ -120,12 +121,12 @@ class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener,
} }
viewModel.load() viewModel.load()
progressBar.show() binding.progressBar.show()
} }
private fun refreshAnnouncements() { private fun refreshAnnouncements() {
viewModel.load() viewModel.load()
swipeRefreshLayout.isRefreshing = true binding.swipeRefreshLayout.isRefreshing = true
} }
override fun openReactionPicker(announcementId: String, target: View) { override fun openReactionPicker(announcementId: String, target: View) {

View file

@ -18,7 +18,6 @@ package com.keylesspalace.tusky.components.compose
import android.Manifest import android.Manifest
import android.app.Activity import android.app.Activity
import android.app.ProgressDialog import android.app.ProgressDialog
import android.app.TimePickerDialog
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences 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.makeCaptionDialog
import com.keylesspalace.tusky.components.compose.dialog.showAddPollDialog import com.keylesspalace.tusky.components.compose.dialog.showAddPollDialog
import com.keylesspalace.tusky.components.compose.view.ComposeOptionsListener 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.AccountEntity
import com.keylesspalace.tusky.db.DraftAttachment import com.keylesspalace.tusky.db.DraftAttachment
import com.keylesspalace.tusky.di.Injectable 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.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp import com.mikepenz.iconics.utils.sizeDp
import kotlinx.android.parcel.Parcelize import kotlinx.parcelize.Parcelize
import kotlinx.android.synthetic.main.activity_compose.*
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.util.* import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
@ -90,7 +90,7 @@ class ComposeActivity : BaseActivity(),
OnEmojiSelectedListener, OnEmojiSelectedListener,
Injectable, Injectable,
InputConnectionCompat.OnCommitContentListener, InputConnectionCompat.OnCommitContentListener,
TimePickerDialog.OnTimeSetListener { ComposeScheduleView.OnTimeSetListener {
@Inject @Inject
lateinit var viewModelFactory: ViewModelFactory lateinit var viewModelFactory: ViewModelFactory
@ -109,17 +109,20 @@ class ComposeActivity : BaseActivity(),
private val viewModel: ComposeViewModel by viewModels { viewModelFactory } private val viewModel: ComposeViewModel by viewModels { viewModelFactory }
private val binding by viewBinding(ActivityComposeBinding::inflate)
private val maxUploadMediaNumber = 4 private val maxUploadMediaNumber = 4
private var mediaCount = 0 private var mediaCount = 0
public override fun onCreate(savedInstanceState: Bundle?) { public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val preferences = PreferenceManager.getDefaultSharedPreferences(this) val preferences = PreferenceManager.getDefaultSharedPreferences(this)
val theme = preferences.getString("appTheme", ThemeUtils.APP_THEME_DEFAULT) val theme = preferences.getString("appTheme", ThemeUtils.APP_THEME_DEFAULT)
if (theme == "black") { if (theme == "black") {
setTheme(R.style.TuskyDialogActivityBlackTheme) setTheme(R.style.TuskyDialogActivityBlackTheme)
} }
setContentView(R.layout.activity_compose) setContentView(binding.root)
setupActionBar() setupActionBar()
// do not do anything when not logged in, activity will be finished in super.onCreate() anyway // 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 onRemove = this::removeMediaFromQueue
) )
composeMediaPreviewBar.layoutManager = binding.composeMediaPreviewBar.layoutManager =
LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false) LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
composeMediaPreviewBar.adapter = mediaAdapter binding.composeMediaPreviewBar.adapter = mediaAdapter
composeMediaPreviewBar.itemAnimator = null binding.composeMediaPreviewBar.itemAnimator = null
subscribeToUpdates(mediaAdapter) subscribeToUpdates(mediaAdapter)
setupButtons() setupButtons()
@ -154,11 +157,11 @@ class ComposeActivity : BaseActivity(),
setupReplyViews(composeOptions?.replyingStatusAuthor, composeOptions?.replyingStatusContent) setupReplyViews(composeOptions?.replyingStatusAuthor, composeOptions?.replyingStatusContent)
val tootText = composeOptions?.tootText val tootText = composeOptions?.tootText
if (!tootText.isNullOrEmpty()) { if (!tootText.isNullOrEmpty()) {
composeEditField.setText(tootText) binding.composeEditField.setText(tootText)
} }
if (!composeOptions?.scheduledAt.isNullOrEmpty()) { if (!composeOptions?.scheduledAt.isNullOrEmpty()) {
composeScheduleView.setDateTime(composeOptions?.scheduledAt) binding.composeScheduleView.setDateTime(composeOptions?.scheduledAt)
} }
setupComposeField(preferences, viewModel.startingText) setupComposeField(preferences, viewModel.startingText)
@ -198,14 +201,14 @@ class ComposeActivity : BaseActivity(),
} }
if (shareBody.isNotBlank()) { if (shareBody.isNotBlank()) {
val start = composeEditField.selectionStart.coerceAtLeast(0) val start = binding.composeEditField.selectionStart.coerceAtLeast(0)
val end = composeEditField.selectionEnd.coerceAtLeast(0) val end = binding.composeEditField.selectionEnd.coerceAtLeast(0)
val left = min(start, end) val left = min(start, end)
val right = max(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 // move edittext cursor to first when shareBody parsed
composeEditField.text.insert(0, "\n") binding.composeEditField.text.insert(0, "\n")
composeEditField.setSelection(0) binding.composeEditField.setSelection(0)
} }
} }
} }
@ -214,58 +217,58 @@ class ComposeActivity : BaseActivity(),
private fun setupReplyViews(replyingStatusAuthor: String?, replyingStatusContent: String?) { private fun setupReplyViews(replyingStatusAuthor: String?, replyingStatusContent: String?) {
if (replyingStatusAuthor != null) { if (replyingStatusAuthor != null) {
composeReplyView.show() binding.composeReplyView.show()
composeReplyView.text = getString(R.string.replying_to, replyingStatusAuthor) binding.composeReplyView.text = getString(R.string.replying_to, replyingStatusAuthor)
val arrowDownIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_arrow_drop_down).apply { sizeDp = 12 } val arrowDownIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_arrow_drop_down).apply { sizeDp = 12 }
ThemeUtils.setDrawableTint(this, arrowDownIcon, android.R.attr.textColorTertiary) ThemeUtils.setDrawableTint(this, arrowDownIcon, android.R.attr.textColorTertiary)
composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, arrowDownIcon, null) binding.composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, arrowDownIcon, null)
composeReplyView.setOnClickListener { binding.composeReplyView.setOnClickListener {
TransitionManager.beginDelayedTransition(composeReplyContentView.parent as ViewGroup) TransitionManager.beginDelayedTransition(binding.composeReplyContentView.parent as ViewGroup)
if (composeReplyContentView.isVisible) { if (binding.composeReplyContentView.isVisible) {
composeReplyContentView.hide() binding.composeReplyContentView.hide()
composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, arrowDownIcon, null) binding.composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, arrowDownIcon, null)
} else { } else {
composeReplyContentView.show() binding.composeReplyContentView.show()
val arrowUpIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_arrow_drop_up).apply { sizeDp = 12 } val arrowUpIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_arrow_drop_up).apply { sizeDp = 12 }
ThemeUtils.setDrawableTint(this, arrowUpIcon, android.R.attr.textColorTertiary) 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?) { private fun setupContentWarningField(startingContentWarning: String?) {
if (startingContentWarning != null) { if (startingContentWarning != null) {
composeContentWarningField.setText(startingContentWarning) binding.composeContentWarningField.setText(startingContentWarning)
} }
composeContentWarningField.onTextChanged { _, _, _, _ -> updateVisibleCharactersLeft() } binding.composeContentWarningField.onTextChanged { _, _, _, _ -> updateVisibleCharactersLeft() }
} }
private fun setupComposeField(preferences: SharedPreferences, startingText: String?) { 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( ComposeAutoCompleteAdapter(
this, this,
preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false), preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false),
preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
) )
) )
composeEditField.setTokenizer(ComposeTokenizer()) binding.composeEditField.setTokenizer(ComposeTokenizer())
composeEditField.setText(startingText) binding.composeEditField.setText(startingText)
composeEditField.setSelection(composeEditField.length()) binding.composeEditField.setSelection(binding.composeEditField.length())
val mentionColour = composeEditField.linkTextColors.defaultColor val mentionColour = binding.composeEditField.linkTextColors.defaultColor
highlightSpans(composeEditField.text, mentionColour) highlightSpans(binding.composeEditField.text, mentionColour)
composeEditField.afterTextChanged { editable -> binding.composeEditField.afterTextChanged { editable ->
highlightSpans(editable, mentionColour) highlightSpans(editable, mentionColour)
updateVisibleCharactersLeft() updateVisibleCharactersLeft()
} }
@ -273,7 +276,7 @@ class ComposeActivity : BaseActivity(),
// work around Android platform bug -> https://issuetracker.google.com/issues/67102093 // work around Android platform bug -> https://issuetracker.google.com/issues/67102093
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.O if (Build.VERSION.SDK_INT == Build.VERSION_CODES.O
|| Build.VERSION.SDK_INT == Build.VERSION_CODES.O_MR1) { || 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 -> viewModel.instanceParams.observe { instanceData ->
maximumTootCharacters = instanceData.maxChars maximumTootCharacters = instanceData.maxChars
updateVisibleCharactersLeft() updateVisibleCharactersLeft()
composeScheduleButton.visible(instanceData.supportsScheduled) binding.composeScheduleButton.visible(instanceData.supportsScheduled)
} }
viewModel.emoji.observe { emoji -> setEmojiList(emoji) } viewModel.emoji.observe { emoji -> setEmojiList(emoji) }
combineLiveData(viewModel.markMediaAsSensitive, viewModel.showContentWarning) { markSensitive, showContentWarning -> combineLiveData(viewModel.markMediaAsSensitive, viewModel.showContentWarning) { markSensitive, showContentWarning ->
@ -296,19 +299,19 @@ class ComposeActivity : BaseActivity(),
mediaAdapter.submitList(media) mediaAdapter.submitList(media)
if (media.size != mediaCount) { if (media.size != mediaCount) {
mediaCount = media.size mediaCount = media.size
composeMediaPreviewBar.visible(media.isNotEmpty()) binding.composeMediaPreviewBar.visible(media.isNotEmpty())
updateSensitiveMediaToggle(viewModel.markMediaAsSensitive.value != false, viewModel.showContentWarning.value != false) updateSensitiveMediaToggle(viewModel.markMediaAsSensitive.value != false, viewModel.showContentWarning.value != false)
} }
} }
viewModel.poll.observe { poll -> viewModel.poll.observe { poll ->
pollPreview.visible(poll != null) binding.pollPreview.visible(poll != null)
poll?.let(pollPreview::setPoll) poll?.let(binding.pollPreview::setPoll)
} }
viewModel.scheduledAt.observe { scheduledAt -> viewModel.scheduledAt.observe { scheduledAt ->
if (scheduledAt == null) { if (scheduledAt == null) {
composeScheduleView.resetSchedule() binding.composeScheduleView.resetSchedule()
} else { } else {
composeScheduleView.setDateTime(scheduledAt) binding.composeScheduleView.setDateTime(scheduledAt)
} }
updateScheduleButton() updateScheduleButton()
} }
@ -316,7 +319,7 @@ class ComposeActivity : BaseActivity(),
val active = poll == null val active = poll == null
&& media!!.size != 4 && media!!.size != 4
&& (media.isEmpty() || media.first().type == QueuedMedia.Type.IMAGE) && (media.isEmpty() || media.first().type == QueuedMedia.Type.IMAGE)
enableButton(composeAddMediaButton, active, active) enableButton(binding.composeAddMediaButton, active, active)
enablePollButton(media.isNullOrEmpty()) enablePollButton(media.isNullOrEmpty())
}.subscribe() }.subscribe()
viewModel.uploadError.observe { viewModel.uploadError.observe {
@ -324,51 +327,52 @@ class ComposeActivity : BaseActivity(),
} }
viewModel.setupComplete.observe { viewModel.setupComplete.observe {
// Focus may have changed during view model setup, ensure initial focus is on the edit field // Focus may have changed during view model setup, ensure initial focus is on the edit field
composeEditField.requestFocus() binding.composeEditField.requestFocus()
} }
} }
} }
private fun setupButtons() { private fun setupButtons() {
composeOptionsBottomSheet.listener = this binding.composeOptionsBottomSheet.listener = this
composeOptionsBehavior = BottomSheetBehavior.from(composeOptionsBottomSheet) composeOptionsBehavior = BottomSheetBehavior.from(binding.composeOptionsBottomSheet)
addMediaBehavior = BottomSheetBehavior.from(addMediaBottomSheet) addMediaBehavior = BottomSheetBehavior.from(binding.addMediaBottomSheet)
scheduleBehavior = BottomSheetBehavior.from(composeScheduleView) scheduleBehavior = BottomSheetBehavior.from(binding.composeScheduleView)
emojiBehavior = BottomSheetBehavior.from(emojiView) emojiBehavior = BottomSheetBehavior.from(binding.emojiView)
enableButton(composeEmojiButton, clickable = false, colorActive = false) enableButton(binding.composeEmojiButton, clickable = false, colorActive = false)
// Setup the interface buttons. // Setup the interface buttons.
composeTootButton.setOnClickListener { onSendClicked() } binding.composeTootButton.setOnClickListener { onSendClicked() }
composeAddMediaButton.setOnClickListener { openPickDialog() } binding.composeAddMediaButton.setOnClickListener { openPickDialog() }
composeToggleVisibilityButton.setOnClickListener { showComposeOptions() } binding.composeToggleVisibilityButton.setOnClickListener { showComposeOptions() }
composeContentWarningButton.setOnClickListener { onContentWarningChanged() } binding.composeContentWarningButton.setOnClickListener { onContentWarningChanged() }
composeEmojiButton.setOnClickListener { showEmojis() } binding.composeEmojiButton.setOnClickListener { showEmojis() }
composeHideMediaButton.setOnClickListener { toggleHideMedia() } binding.composeHideMediaButton.setOnClickListener { toggleHideMedia() }
composeScheduleButton.setOnClickListener { onScheduleClick() } binding.composeScheduleButton.setOnClickListener { onScheduleClick() }
composeScheduleView.setResetOnClickListener { resetSchedule() } binding.composeScheduleView.setResetOnClickListener { resetSchedule() }
atButton.setOnClickListener { atButtonClicked() } binding.composeScheduleView.setListener(this)
hashButton.setOnClickListener { hashButtonClicked() } binding.atButton.setOnClickListener { atButtonClicked() }
binding.hashButton.setOnClickListener { hashButtonClicked() }
val textColor = ThemeUtils.getColor(this, android.R.attr.textColorTertiary) val textColor = ThemeUtils.getColor(this, android.R.attr.textColorTertiary)
val cameraIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_camera_alt).apply { colorInt = textColor; sizeDp = 18 } 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 } 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 } 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() } binding.actionPhotoTake.setOnClickListener { initiateCameraApp() }
actionPhotoPick.setOnClickListener { onMediaPick() } binding.actionPhotoPick.setOnClickListener { onMediaPick() }
addPollTextActionTextView.setOnClickListener { openPollDialog() } binding.addPollTextActionTextView.setOnClickListener { openPollDialog() }
} }
private fun setupActionBar() { private fun setupActionBar() {
setSupportActionBar(toolbar) setSupportActionBar(binding.toolbar)
supportActionBar?.run { supportActionBar?.run {
title = null title = null
setDisplayHomeAsUpEnabled(true) setDisplayHomeAsUpEnabled(true)
@ -387,40 +391,40 @@ class ComposeActivity : BaseActivity(),
val animateAvatars = preferences.getBoolean("animateGifAvatars", false) val animateAvatars = preferences.getBoolean("animateGifAvatars", false)
loadAvatar( loadAvatar(
activeAccount.profilePictureUrl, activeAccount.profilePictureUrl,
composeAvatar, binding.composeAvatar,
avatarSize / 8, avatarSize / 8,
animateAvatars animateAvatars
) )
composeAvatar.contentDescription = getString(R.string.compose_active_account_description, binding.composeAvatar.contentDescription = getString(R.string.compose_active_account_description,
activeAccount.fullName) activeAccount.fullName)
} }
private fun replaceTextAtCaret(text: CharSequence) { private fun replaceTextAtCaret(text: CharSequence) {
// If you select "backward" in an editable, you get SelectionStart > SelectionEnd // If you select "backward" in an editable, you get SelectionStart > SelectionEnd
val start = composeEditField.selectionStart.coerceAtMost(composeEditField.selectionEnd) val start = binding.composeEditField.selectionStart.coerceAtMost(binding.composeEditField.selectionEnd)
val end = composeEditField.selectionStart.coerceAtLeast(composeEditField.selectionEnd) val end = binding.composeEditField.selectionStart.coerceAtLeast(binding.composeEditField.selectionEnd)
val textToInsert = if (start > 0 && !composeEditField.text[start - 1].isWhitespace()) { val textToInsert = if (start > 0 && !binding.composeEditField.text[start - 1].isWhitespace()) {
" $text" " $text"
} else { } else {
text text
} }
composeEditField.text.replace(start, end, textToInsert) binding.composeEditField.text.replace(start, end, textToInsert)
// Set the cursor after the inserted text // Set the cursor after the inserted text
composeEditField.setSelection(start + text.length) binding.composeEditField.setSelection(start + text.length)
} }
fun prependSelectedWordsWith(text: CharSequence) { fun prependSelectedWordsWith(text: CharSequence) {
// If you select "backward" in an editable, you get SelectionStart > SelectionEnd // If you select "backward" in an editable, you get SelectionStart > SelectionEnd
val start = composeEditField.selectionStart.coerceAtMost(composeEditField.selectionEnd) val start = binding.composeEditField.selectionStart.coerceAtMost(binding.composeEditField.selectionEnd)
val end = composeEditField.selectionStart.coerceAtLeast(composeEditField.selectionEnd) val end = binding.composeEditField.selectionStart.coerceAtLeast(binding.composeEditField.selectionEnd)
val editorText = composeEditField.text val editorText = binding.composeEditField.text
if (start == end) { if (start == end) {
// No selection, just insert text at caret // No selection, just insert text at caret
editorText.insert(start, text) editorText.insert(start, text)
// Set the cursor after the inserted text // Set the cursor after the inserted text
composeEditField.setSelection(start + text.length) binding.composeEditField.setSelection(start + text.length)
} else { } else {
var wasWord: Boolean var wasWord: Boolean
var isWord = end < editorText.length && !Character.isWhitespace(editorText[end]) var isWord = end < editorText.length && !Character.isWhitespace(editorText[end])
@ -446,7 +450,7 @@ class ComposeActivity : BaseActivity(),
} }
// Keep the same text (including insertions) selected // 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) { 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 //necessary so snackbar is shown over everything
bar.view.elevation = resources.getDimension(R.dimen.compose_activity_snackbar_elevation) bar.view.elevation = resources.getDimension(R.dimen.compose_activity_snackbar_elevation)
bar.show() bar.show()
@ -477,49 +481,49 @@ class ComposeActivity : BaseActivity(),
private fun updateSensitiveMediaToggle(markMediaSensitive: Boolean, contentWarningShown: Boolean) { private fun updateSensitiveMediaToggle(markMediaSensitive: Boolean, contentWarningShown: Boolean) {
if (viewModel.media.value.isNullOrEmpty()) { if (viewModel.media.value.isNullOrEmpty()) {
composeHideMediaButton.hide() binding.composeHideMediaButton.hide()
} else { } else {
composeHideMediaButton.show() binding.composeHideMediaButton.show()
@ColorInt val color = if (contentWarningShown) { @ColorInt val color = if (contentWarningShown) {
composeHideMediaButton.setImageResource(R.drawable.ic_hide_media_24dp) binding.composeHideMediaButton.setImageResource(R.drawable.ic_hide_media_24dp)
composeHideMediaButton.isClickable = false binding.composeHideMediaButton.isClickable = false
ContextCompat.getColor(this, R.color.transparent_chinwag_green) ContextCompat.getColor(this, R.color.transparent_chinwag_green)
} else { } else {
composeHideMediaButton.isClickable = true binding.composeHideMediaButton.isClickable = true
if (markMediaSensitive) { 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) ContextCompat.getColor(this, R.color.chinwag_green)
} else { } else {
composeHideMediaButton.setImageResource(R.drawable.ic_eye_24dp) binding.composeHideMediaButton.setImageResource(R.drawable.ic_eye_24dp)
ThemeUtils.getColor(this, android.R.attr.textColorTertiary) 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() { 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) ThemeUtils.getColor(this, android.R.attr.textColorTertiary)
} else { } else {
ContextCompat.getColor(this, R.color.chinwag_green) 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) { private fun enableButtons(enable: Boolean) {
composeAddMediaButton.isClickable = enable binding.composeAddMediaButton.isClickable = enable
composeToggleVisibilityButton.isClickable = enable binding.composeToggleVisibilityButton.isClickable = enable
composeEmojiButton.isClickable = enable binding.composeEmojiButton.isClickable = enable
composeHideMediaButton.isClickable = enable binding.composeHideMediaButton.isClickable = enable
composeScheduleButton.isClickable = enable binding.composeScheduleButton.isClickable = enable
composeTootButton.isEnabled = enable binding.composeTootButton.isEnabled = enable
} }
private fun setStatusVisibility(visibility: Status.Visibility) { private fun setStatusVisibility(visibility: Status.Visibility) {
composeOptionsBottomSheet.setStatusVisibility(visibility) binding.composeOptionsBottomSheet.setStatusVisibility(visibility)
composeTootButton.setStatusVisibility(visibility) binding.composeTootButton.setStatusVisibility(visibility)
val iconRes = when (visibility) { val iconRes = when (visibility) {
Status.Visibility.PUBLIC -> R.drawable.ic_public_24dp Status.Visibility.PUBLIC -> R.drawable.ic_public_24dp
@ -528,7 +532,7 @@ class ComposeActivity : BaseActivity(),
Status.Visibility.UNLISTED -> R.drawable.ic_lock_open_24dp Status.Visibility.UNLISTED -> R.drawable.ic_lock_open_24dp
else -> R.drawable.ic_lock_open_24dp else -> R.drawable.ic_lock_open_24dp
} }
composeToggleVisibilityButton.setImageResource(iconRes) binding.composeToggleVisibilityButton.setImageResource(iconRes)
} }
private fun showComposeOptions() { private fun showComposeOptions() {
@ -544,7 +548,7 @@ class ComposeActivity : BaseActivity(),
private fun onScheduleClick() { private fun onScheduleClick() {
if (viewModel.scheduledAt.value == null) { if (viewModel.scheduledAt.value == null) {
composeScheduleView.openPickDateDialog() binding.composeScheduleView.openPickDateDialog()
} else { } else {
showScheduleView() showScheduleView()
} }
@ -562,7 +566,7 @@ class ComposeActivity : BaseActivity(),
} }
private fun showEmojis() { private fun showEmojis() {
emojiView.adapter?.let { binding.emojiView.adapter?.let {
if (it.itemCount == 0) { if (it.itemCount == 0) {
val errorMessage = getString(R.string.error_no_custom_emojis, accountManager.activeAccount!!.domain) val errorMessage = getString(R.string.error_no_custom_emojis, accountManager.activeAccount!!.domain)
Toast.makeText(this, errorMessage, Toast.LENGTH_SHORT).show() 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) val layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
layoutParams.setMargins(margin, margin, margin, marginBottom) layoutParams.setMargins(margin, margin, margin, marginBottom)
pollPreview.layoutParams = layoutParams binding.pollPreview.layoutParams = layoutParams
pollPreview.setOnClickListener { binding.pollPreview.setOnClickListener {
val popup = PopupMenu(this, pollPreview) val popup = PopupMenu(this, binding.pollPreview)
val editId = 1 val editId = 1
val removeId = 2 val removeId = 2
popup.menu.add(0, editId, 0, R.string.edit_poll) popup.menu.add(0, editId, 0, R.string.edit_poll)
@ -646,7 +650,7 @@ class ComposeActivity : BaseActivity(),
private fun removePoll() { private fun removePoll() {
viewModel.poll.value = null viewModel.poll.value = null
pollPreview.hide() binding.pollPreview.hide()
} }
override fun onVisibilityChanged(visibility: Status.Visibility) { override fun onVisibilityChanged(visibility: Status.Visibility) {
@ -657,39 +661,39 @@ class ComposeActivity : BaseActivity(),
@VisibleForTesting @VisibleForTesting
fun calculateTextLength(): Int { fun calculateTextLength(): Int {
var offset = 0 var offset = 0
val urlSpans = composeEditField.urls val urlSpans = binding.composeEditField.urls
if (urlSpans != null) { if (urlSpans != null) {
for (span in urlSpans) { for (span in urlSpans) {
offset += max(0, span.url.length - MAXIMUM_URL_LENGTH) offset += max(0, span.url.length - MAXIMUM_URL_LENGTH)
} }
} }
var length = composeEditField.length() - offset var length = binding.composeEditField.length() - offset
if (viewModel.showContentWarning.value!!) { if (viewModel.showContentWarning.value!!) {
length += composeContentWarningField.length() length += binding.composeContentWarningField.length()
} }
return length return length
} }
private fun updateVisibleCharactersLeft() { private fun updateVisibleCharactersLeft() {
val remainingLength = maximumTootCharacters - calculateTextLength() 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) { val textColor = if (remainingLength < 0) {
ContextCompat.getColor(this, R.color.tusky_red) ContextCompat.getColor(this, R.color.tusky_red)
} else { } else {
ThemeUtils.getColor(this, android.R.attr.textColorTertiary) ThemeUtils.getColor(this, android.R.attr.textColorTertiary)
} }
composeCharactersLeftView.setTextColor(textColor) binding.composeCharactersLeftView.setTextColor(textColor)
} }
private fun onContentWarningChanged() { private fun onContentWarningChanged() {
val showWarning = composeContentWarningBar.isGone val showWarning = binding.composeContentWarningBar.isGone
viewModel.contentWarningChanged(showWarning) viewModel.contentWarningChanged(showWarning)
updateVisibleCharactersLeft() updateVisibleCharactersLeft()
} }
private fun verifyScheduledTime(): Boolean { 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() { private fun onSendClicked() {
@ -724,14 +728,14 @@ class ComposeActivity : BaseActivity(),
private fun sendStatus() { private fun sendStatus() {
enableButtons(false) enableButtons(false)
val contentText = composeEditField.text.toString() val contentText = binding.composeEditField.text.toString()
var spoilerText = "" var spoilerText = ""
if (viewModel.showContentWarning.value!!) { if (viewModel.showContentWarning.value!!) {
spoilerText = composeContentWarningField.text.toString() spoilerText = binding.composeContentWarningField.text.toString()
} }
val characterCount = calculateTextLength() val characterCount = calculateTextLength()
if ((characterCount <= 0 || contentText.isBlank()) && viewModel.media.value!!.isEmpty()) { 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) enableButtons(true)
} else if (characterCount <= maximumTootCharacters) { } else if (characterCount <= maximumTootCharacters) {
if (viewModel.media.value!!.isNotEmpty()) { if (viewModel.media.value!!.isNotEmpty()) {
@ -746,7 +750,7 @@ class ComposeActivity : BaseActivity(),
}) })
} else { } else {
composeEditField.error = getString(R.string.error_compose_character_limit) binding.composeEditField.error = getString(R.string.error_compose_character_limit)
enableButtons(true) enableButtons(true)
} }
} }
@ -757,7 +761,7 @@ class ComposeActivity : BaseActivity(),
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
initiateMediaPicking() initiateMediaPicking()
} else { } 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 { Snackbar.LENGTH_SHORT).apply {
} }
@ -812,12 +816,12 @@ class ComposeActivity : BaseActivity(),
} }
private fun enablePollButton(enable: Boolean) { private fun enablePollButton(enable: Boolean) {
addPollTextActionTextView.isEnabled = enable binding.addPollTextActionTextView.isEnabled = enable
val textColor = ThemeUtils.getColor(this, val textColor = ThemeUtils.getColor(this,
if (enable) android.R.attr.textColorTertiary if (enable) android.R.attr.textColorTertiary
else R.attr.textColorDisabled) else R.attr.textColorDisabled)
addPollTextActionTextView.setTextColor(textColor) binding.addPollTextActionTextView.setTextColor(textColor)
addPollTextActionTextView.compoundDrawablesRelative[0].colorFilter = PorterDuffColorFilter(textColor, PorterDuff.Mode.SRC_IN) binding.addPollTextActionTextView.compoundDrawablesRelative[0].colorFilter = PorterDuffColorFilter(textColor, PorterDuff.Mode.SRC_IN)
} }
private fun removeMediaFromQueue(item: QueuedMedia) { private fun removeMediaFromQueue(item: QueuedMedia) {
@ -835,7 +839,7 @@ class ComposeActivity : BaseActivity(),
val count = clipData.itemCount val count = clipData.itemCount
if (mediaCount + count > maxUploadMediaNumber) { if (mediaCount + count > maxUploadMediaNumber) {
// check if exist media + upcoming media > 4, then prob error message. // 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 { } else {
// if not grater then 4, upload all multiple media. // if not grater then 4, upload all multiple media.
for (i in 0 until count) { for (i in 0 until count) {
@ -878,19 +882,18 @@ class ComposeActivity : BaseActivity(),
} }
private fun showContentWarning(show: Boolean) { private fun showContentWarning(show: Boolean) {
TransitionManager.beginDelayedTransition(composeContentWarningBar.parent as ViewGroup) TransitionManager.beginDelayedTransition(binding.composeContentWarningBar.parent as ViewGroup)
@ColorInt val color = if (show) { @ColorInt val color = if (show) {
composeContentWarningBar.show() binding.composeContentWarningBar.show()
composeContentWarningField.setSelection(composeContentWarningField.text.length) binding.composeContentWarningField.setSelection(binding.composeContentWarningField.text.length)
composeContentWarningField.requestFocus() binding.composeContentWarningField.requestFocus()
ContextCompat.getColor(this, R.color.chinwag_green) ContextCompat.getColor(this, R.color.chinwag_green)
} else { } else {
composeContentWarningBar.hide() binding.composeContentWarningBar.hide()
composeEditField.requestFocus() binding.composeEditField.requestFocus()
ThemeUtils.getColor(this, android.R.attr.textColorTertiary) 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 { override fun onOptionsItemSelected(item: MenuItem): Boolean {
@ -938,8 +941,8 @@ class ComposeActivity : BaseActivity(),
} }
private fun handleCloseButton() { private fun handleCloseButton() {
val contentText = composeEditField.text.toString() val contentText = binding.composeEditField.text.toString()
val contentWarning = composeContentWarningField.text.toString() val contentWarning = binding.composeContentWarningField.text.toString()
if (viewModel.didChange(contentText, contentWarning)) { if (viewModel.didChange(contentText, contentWarning)) {
AlertDialog.Builder(this) AlertDialog.Builder(this)
.setMessage(R.string.compose_save_draft) .setMessage(R.string.compose_save_draft)
@ -973,8 +976,8 @@ class ComposeActivity : BaseActivity(),
private fun setEmojiList(emojiList: List<Emoji>?) { private fun setEmojiList(emojiList: List<Emoji>?) {
if (emojiList != null) { if (emojiList != null) {
emojiView.adapter = EmojiAdapter(emojiList, this@ComposeActivity) binding.emojiView.adapter = EmojiAdapter(emojiList, this@ComposeActivity)
enableButton(composeEmojiButton, true, emojiList.isNotEmpty()) enableButton(binding.composeEmojiButton, true, emojiList.isNotEmpty())
} }
} }
@ -992,9 +995,8 @@ class ComposeActivity : BaseActivity(),
} }
} }
override fun onTimeSet(view: TimePicker, hourOfDay: Int, minute: Int) { override fun onTimeSet(time: String) {
composeScheduleView.onTimeSet(hourOfDay, minute) viewModel.updateScheduledAt(time)
viewModel.updateScheduledAt(composeScheduleView.time)
if (verifyScheduledTime()) { if (verifyScheduledTime()) {
scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN
} else { } else {

View file

@ -103,7 +103,7 @@ class MediaPreviewAdapter(
progressImageView.layoutParams = layoutParams progressImageView.layoutParams = layoutParams
progressImageView.scaleType = ImageView.ScaleType.CENTER_CROP progressImageView.scaleType = ImageView.ScaleType.CENTER_CROP
progressImageView.setOnClickListener { progressImageView.setOnClickListener {
onMediaClick(adapterPosition, progressImageView) onMediaClick(bindingAdapterPosition, progressImageView)
} }
} }
} }

View file

@ -22,9 +22,8 @@ import android.view.LayoutInflater
import android.view.WindowManager import android.view.WindowManager
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.adapter.AddPollOptionsAdapter import com.keylesspalace.tusky.databinding.DialogAddPollBinding
import com.keylesspalace.tusky.entity.NewPoll import com.keylesspalace.tusky.entity.NewPoll
import kotlinx.android.synthetic.main.dialog_add_poll.view.*
fun showAddPollDialog( fun showAddPollDialog(
context: Context, context: Context,
@ -34,12 +33,12 @@ fun showAddPollDialog(
onUpdatePoll: (NewPoll) -> Unit 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) val dialog = AlertDialog.Builder(context)
.setIcon(R.drawable.ic_poll_24dp) .setIcon(R.drawable.ic_poll_24dp)
.setTitle(R.string.create_poll_title) .setTitle(R.string.create_poll_title)
.setView(view) .setView(binding.root)
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(android.R.string.ok, null) .setPositiveButton(android.R.string.ok, null)
.create() .create()
@ -48,7 +47,7 @@ fun showAddPollDialog(
options = poll?.options?.toMutableList() ?: mutableListOf("", ""), options = poll?.options?.toMutableList() ?: mutableListOf("", ""),
maxOptionLength = maxOptionLength, maxOptionLength = maxOptionLength,
onOptionRemoved = { valid -> onOptionRemoved = { valid ->
view.addChoiceButton.isEnabled = true binding.addChoiceButton.isEnabled = true
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = valid dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = valid
}, },
onOptionChanged = { 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) { if (adapter.itemCount < maxOptionCount) {
adapter.addChoice() adapter.addChoice()
} }
@ -71,14 +70,14 @@ fun showAddPollDialog(
it <= poll?.expiresIn ?: 0 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 { dialog.setOnShowListener {
val button = dialog.getButton(AlertDialog.BUTTON_POSITIVE) val button = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
button.setOnClickListener { button.setOnClickListener {
val selectedPollDurationId = view.pollDurationSpinner.selectedItemPosition val selectedPollDurationId = binding.pollDurationSpinner.selectedItemPosition
val pollDuration = context.resources val pollDuration = context.resources
.getIntArray(R.array.poll_duration_values)[selectedPollDurationId] .getIntArray(R.array.poll_duration_values)[selectedPollDurationId]
@ -86,7 +85,7 @@ fun showAddPollDialog(
onUpdatePoll(NewPoll( onUpdatePoll(NewPoll(
options = adapter.pollOptions, options = adapter.pollOptions,
expiresIn = pollDuration, expiresIn = pollDuration,
multiple = view.multipleChoicesCheckBox.isChecked multiple = binding.multipleChoicesCheckBox.isChecked
)) ))
dialog.dismiss() dialog.dismiss()
@ -97,5 +96,4 @@ fun showAddPollDialog(
// make the dialog focusable so the keyboard does not stay behind it // 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) dialog.window?.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM)
} }

View file

@ -13,17 +13,16 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not, * You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */ * see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.adapter package com.keylesspalace.tusky.components.compose.dialog
import android.text.InputFilter import android.text.InputFilter
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageButton
import androidx.recyclerview.widget.RecyclerView 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.R
import com.keylesspalace.tusky.databinding.ItemAddPollOptionBinding
import com.keylesspalace.tusky.util.BindingHolder
import com.keylesspalace.tusky.util.onTextChanged import com.keylesspalace.tusky.util.onTextChanged
import com.keylesspalace.tusky.util.visible import com.keylesspalace.tusky.util.visible
@ -32,7 +31,7 @@ class AddPollOptionsAdapter(
private val maxOptionLength: Int, private val maxOptionLength: Int,
private val onOptionRemoved: (Boolean) -> Unit, private val onOptionRemoved: (Boolean) -> Unit,
private val onOptionChanged: (Boolean) -> Unit private val onOptionChanged: (Boolean) -> Unit
): RecyclerView.Adapter<ViewHolder>() { ): RecyclerView.Adapter<BindingHolder<ItemAddPollOptionBinding>>() {
val pollOptions: List<String> val pollOptions: List<String>
get() = options.toList() get() = options.toList()
@ -42,11 +41,12 @@ class AddPollOptionsAdapter(
notifyItemInserted(options.size - 1) notifyItemInserted(options.size - 1)
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemAddPollOptionBinding> {
val holder = ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_add_poll_option, parent, false)) val binding = ItemAddPollOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false)
holder.editText.filters = arrayOf(InputFilter.LengthFilter(maxOptionLength)) val holder = BindingHolder(binding)
binding.optionEditText.filters = arrayOf(InputFilter.LengthFilter(maxOptionLength))
holder.editText.onTextChanged { s, _, _, _ -> binding.optionEditText.onTextChanged { s, _, _, _ ->
val pos = holder.adapterPosition val pos = holder.adapterPosition
if(pos != RecyclerView.NO_POSITION) { if(pos != RecyclerView.NO_POSITION) {
options[pos] = s.toString() options[pos] = s.toString()
@ -59,15 +59,15 @@ class AddPollOptionsAdapter(
override fun getItemCount() = options.size override fun getItemCount() = options.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: BindingHolder<ItemAddPollOptionBinding>, position: Int) {
holder.editText.setText(options[position]) 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.binding.deleteButton.setOnClickListener {
holder.editText.clearFocus() holder.binding.optionEditText.clearFocus()
options.removeAt(holder.adapterPosition) options.removeAt(holder.adapterPosition)
notifyItemRemoved(holder.adapterPosition) notifyItemRemoved(holder.adapterPosition)
onOptionRemoved(validateInput()) onOptionRemoved(validateInput())
@ -81,12 +81,4 @@ class AddPollOptionsAdapter(
return true 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)
} }

View file

@ -31,6 +31,7 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import at.connyduck.sparkbutton.helpers.Utils import at.connyduck.sparkbutton.helpers.Utils
import com.bumptech.glide.Glide 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.target.CustomTarget
import com.bumptech.glide.request.transition.Transition import com.bumptech.glide.request.transition.Transition
import com.github.chrisbanes.photoview.PhotoView import com.github.chrisbanes.photoview.PhotoView
@ -63,8 +64,8 @@ fun <T> T.makeCaptionDialog(existingDescription: String?,
(imageView.layoutParams as LinearLayout.LayoutParams).setMargins(0, margin, 0, 0) (imageView.layoutParams as LinearLayout.LayoutParams).setMargins(0, margin, 0, 0)
val input = EditText(this) val input = EditText(this)
input.hint = getString(R.string.hint_describe_for_visually_impaired, input.hint = resources.getQuantityString(R.plurals.hint_describe_for_visually_impaired,
MEDIA_DESCRIPTION_CHARACTER_LIMIT) MEDIA_DESCRIPTION_CHARACTER_LIMIT, MEDIA_DESCRIPTION_CHARACTER_LIMIT)
dialogLayout.addView(input) dialogLayout.addView(input)
(input.layoutParams as LinearLayout.LayoutParams).setMargins(margin, margin, margin, margin) (input.layoutParams as LinearLayout.LayoutParams).setMargins(margin, margin, margin, margin)
input.setLines(2) input.setLines(2)
@ -97,12 +98,14 @@ fun <T> T.makeCaptionDialog(existingDescription: String?,
dialog.show() dialog.show()
// Load the image and manually set it into the ImageView because it doesn't have a fixed // Load the image and manually set it into the ImageView because it doesn't have a fixed size.
// size. Maybe we should limit the size of CustomTarget
Glide.with(this) Glide.with(this)
.load(previewUri) .load(previewUri)
.into(object : CustomTarget<Drawable>() { .downsample(DownsampleStrategy.CENTER_INSIDE)
override fun onLoadCleared(placeholder: Drawable?) {} .into(object : CustomTarget<Drawable>(4096, 4096) {
override fun onLoadCleared(placeholder: Drawable?) {
imageView.setImageDrawable(placeholder)
}
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) { override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
imageView.setImageDrawable(resource) imageView.setImageDrawable(resource)

View file

@ -17,7 +17,6 @@ package com.keylesspalace.tusky.components.compose.view;
import android.content.Context; import android.content.Context;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.widget.Button; import android.widget.Button;
import android.widget.TextView; 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.CalendarConstraints;
import com.google.android.material.datepicker.DateValidatorPointForward; import com.google.android.material.datepicker.DateValidatorPointForward;
import com.google.android.material.datepicker.MaterialDatePicker; 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.R;
import com.keylesspalace.tusky.fragment.TimePickerFragment;
import java.text.DateFormat; import java.text.DateFormat;
import java.text.ParseException; import java.text.ParseException;
@ -44,6 +44,12 @@ import java.util.TimeZone;
public class ComposeScheduleView extends ConstraintLayout { public class ComposeScheduleView extends ConstraintLayout {
public interface OnTimeSetListener {
void onTimeSet(String time);
}
private OnTimeSetListener listener;
private DateFormat dateFormat; private DateFormat dateFormat;
private DateFormat timeFormat; private DateFormat timeFormat;
private SimpleDateFormat iso8601; private SimpleDateFormat iso8601;
@ -92,6 +98,10 @@ public class ComposeScheduleView extends ConstraintLayout {
setEditIcons(); setEditIcons();
} }
public void setListener(OnTimeSetListener listener) {
this.listener = listener;
}
private void setScheduledDateTime() { private void setScheduledDateTime() {
if (scheduleDateTime == null) { if (scheduleDateTime == null) {
scheduledDateTimeView.setText(""); scheduledDateTimeView.setText("");
@ -144,13 +154,20 @@ public class ComposeScheduleView extends ConstraintLayout {
} }
private void openPickTimeDialog() { private void openPickTimeDialog() {
TimePickerFragment picker = new TimePickerFragment(); MaterialTimePicker.Builder pickerBuilder = new MaterialTimePicker.Builder();
if (scheduleDateTime != null) { if (scheduleDateTime != null) {
Bundle args = new Bundle(); pickerBuilder.setHour(scheduleDateTime.get(Calendar.HOUR_OF_DAY))
args.putInt(TimePickerFragment.PICKER_TIME_HOUR, scheduleDateTime.get(Calendar.HOUR_OF_DAY)); .setMinute(scheduleDateTime.get(Calendar.MINUTE));
args.putInt(TimePickerFragment.PICKER_TIME_MINUTE, scheduleDateTime.get(Calendar.MINUTE));
picker.setArguments(args);
} }
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"); picker.show(((AppCompatActivity) getContext()).getSupportFragmentManager(), "time_picker");
} }
@ -200,11 +217,14 @@ public class ComposeScheduleView extends ConstraintLayout {
openPickTimeDialog(); openPickTimeDialog();
} }
public void onTimeSet(int hourOfDay, int minute) { private void onTimeSet(int hourOfDay, int minute) {
initializeSuggestedTime(); initializeSuggestedTime();
scheduleDateTime.set(Calendar.HOUR_OF_DAY, hourOfDay); scheduleDateTime.set(Calendar.HOUR_OF_DAY, hourOfDay);
scheduleDateTime.set(Calendar.MINUTE, minute); scheduleDateTime.set(Calendar.MINUTE, minute);
setScheduledDateTime(); setScheduledDateTime();
if (listener != null) {
listener.onTimeSet(getTime());
}
} }
public String getTime() { public String getTime() {

View file

@ -17,11 +17,12 @@ package com.keylesspalace.tusky.components.compose.view
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout import android.widget.LinearLayout
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.adapter.PreviewPollOptionsAdapter import com.keylesspalace.tusky.adapter.PreviewPollOptionsAdapter
import com.keylesspalace.tusky.databinding.ViewPollPreviewBinding
import com.keylesspalace.tusky.entity.NewPoll import com.keylesspalace.tusky.entity.NewPoll
import kotlinx.android.synthetic.main.view_poll_preview.view.*
class PollPreviewView @JvmOverloads constructor( class PollPreviewView @JvmOverloads constructor(
context: Context?, context: Context?,
@ -29,11 +30,11 @@ class PollPreviewView @JvmOverloads constructor(
defStyleAttr: Int = 0) defStyleAttr: Int = 0)
: LinearLayout(context, attrs, defStyleAttr) { : LinearLayout(context, attrs, defStyleAttr) {
val adapter = PreviewPollOptionsAdapter() private val adapter = PreviewPollOptionsAdapter()
private val binding = ViewPollPreviewBinding.inflate(LayoutInflater.from(context), this)
init { init {
inflate(context, R.layout.view_poll_preview, this)
orientation = VERTICAL orientation = VERTICAL
setBackgroundResource(R.drawable.card_frame) setBackgroundResource(R.drawable.card_frame)
@ -42,8 +43,7 @@ class PollPreviewView @JvmOverloads constructor(
setPadding(padding, padding, padding, padding) setPadding(padding, padding, padding, padding)
pollPreviewOptions.adapter = adapter binding.pollPreviewOptions.adapter = adapter
} }
fun setPoll(poll: NewPoll){ fun setPoll(poll: NewPoll){
@ -52,13 +52,11 @@ class PollPreviewView @JvmOverloads constructor(
val pollDurationId = resources.getIntArray(R.array.poll_duration_values).indexOfLast { val pollDurationId = resources.getIntArray(R.array.poll_duration_values).indexOfLast {
it <= poll.expiresIn 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?) { override fun setOnClickListener(l: OnClickListener?) {
super.setOnClickListener(l) super.setOnClickListener(l)
adapter.setOnClickListener(l) adapter.setOnClickListener(l)
} }
} }

View file

@ -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 <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.components.conversation package com.keylesspalace.tusky.components.conversation
import android.view.LayoutInflater import android.view.LayoutInflater
@ -10,6 +25,7 @@ import androidx.recyclerview.widget.ListUpdateCallback
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.adapter.NetworkStateViewHolder import com.keylesspalace.tusky.adapter.NetworkStateViewHolder
import com.keylesspalace.tusky.databinding.ItemNetworkStateBinding
import com.keylesspalace.tusky.interfaces.StatusActionListener import com.keylesspalace.tusky.interfaces.StatusActionListener
import com.keylesspalace.tusky.util.NetworkState import com.keylesspalace.tusky.util.NetworkState
import com.keylesspalace.tusky.util.StatusDisplayOptions import com.keylesspalace.tusky.util.StatusDisplayOptions
@ -49,11 +65,15 @@ class ConversationAdapter(
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
return when (viewType) { return when (viewType) {
R.layout.item_network_state -> NetworkStateViewHolder(view, retryCallback) R.layout.item_network_state -> {
R.layout.item_conversation -> ConversationViewHolder(view, statusDisplayOptions, val binding = ItemNetworkStateBinding.inflate(LayoutInflater.from(parent.context), parent, false)
listener) 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") else -> throw IllegalArgumentException("unknown view type $viewType")
} }
} }

View file

@ -167,7 +167,7 @@ fun Account.toEntity() =
ConversationAccountEntity( ConversationAccountEntity(
id, id,
username, username,
displayName.orEmpty(), name,
avatar, avatar,
emojis ?: emptyList() emojis ?: emptyList()
) )

View file

@ -147,7 +147,7 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
/* input filter for TextViews have to be set before text */ /* input filter for TextViews have to be set before text */
if (collapsible && (expanded || TextUtils.isEmpty(spoilerText))) { if (collapsible && (expanded || TextUtils.isEmpty(spoilerText))) {
contentCollapseButton.setOnClickListener(view -> { contentCollapseButton.setOnClickListener(view -> {
int position = getAdapterPosition(); int position = getBindingAdapterPosition();
if (position != RecyclerView.NO_POSITION) if (position != RecyclerView.NO_POSITION)
listener.onContentCollapsedChange(!collapsed, position); listener.onContentCollapsedChange(!collapsed, position);
}); });

View file

@ -28,6 +28,7 @@ import androidx.recyclerview.widget.SimpleItemAnimator
import com.keylesspalace.tusky.AccountActivity import com.keylesspalace.tusky.AccountActivity
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.ViewTagActivity import com.keylesspalace.tusky.ViewTagActivity
import com.keylesspalace.tusky.databinding.FragmentTimelineBinding
import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.fragment.SFragment 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.NetworkState
import com.keylesspalace.tusky.util.StatusDisplayOptions import com.keylesspalace.tusky.util.StatusDisplayOptions
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import kotlinx.android.synthetic.main.fragment_timeline.* import com.keylesspalace.tusky.util.viewBinding
import javax.inject.Inject import javax.inject.Inject
class ConversationsFragment : SFragment(), StatusActionListener, Injectable, ReselectableFragment { 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 viewModel: ConversationsViewModel by viewModels { viewModelFactory }
private val binding by viewBinding(FragmentTimelineBinding::bind)
private lateinit var adapter: ConversationAdapter private lateinit var adapter: ConversationAdapter
private var layoutManager: LinearLayoutManager? = null private var layoutManager: LinearLayoutManager? = null
@ -73,14 +76,14 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
adapter = ConversationAdapter(statusDisplayOptions, this, ::onTopLoaded, viewModel::retry) 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) layoutManager = LinearLayoutManager(view.context)
recyclerView.layoutManager = layoutManager binding.recyclerView.layoutManager = layoutManager
recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
(recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false (binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
progressBar.hide() binding.progressBar.hide()
statusView.hide() binding.statusView.hide()
initSwipeToRefresh() initSwipeToRefresh()
@ -97,16 +100,16 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
private fun initSwipeToRefresh() { private fun initSwipeToRefresh() {
viewModel.refreshState.observe(viewLifecycleOwner) { viewModel.refreshState.observe(viewLifecycleOwner) {
swipeRefreshLayout.isRefreshing = it == NetworkState.LOADING binding.swipeRefreshLayout.isRefreshing = it == NetworkState.LOADING
} }
swipeRefreshLayout.setOnRefreshListener { binding.swipeRefreshLayout.setOnRefreshListener {
viewModel.refresh() viewModel.refresh()
} }
swipeRefreshLayout.setColorSchemeResources(R.color.chinwag_green) binding.swipeRefreshLayout.setColorSchemeResources(R.color.chinwag_green)
} }
private fun onTopLoaded() { private fun onTopLoaded() {
recyclerView.scrollToPosition(0) binding.recyclerView.scrollToPosition(0)
} }
override fun onReblog(reblog: Boolean, position: Int) { override fun onReblog(reblog: Boolean, position: Int) {
@ -183,7 +186,7 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
private fun jumpToTop() { private fun jumpToTop() {
if (isAdded) { if (isAdded) {
layoutManager?.scrollToPosition(0) layoutManager?.scrollToPosition(0)
recyclerView.stopScroll() binding.recyclerView.stopScroll()
} }
} }

View file

@ -23,7 +23,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.keylesspalace.tusky.databinding.ItemDraftBinding import com.keylesspalace.tusky.databinding.ItemDraftBinding
import com.keylesspalace.tusky.db.DraftEntity 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.hide
import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.visible import com.keylesspalace.tusky.util.visible
@ -35,7 +35,7 @@ interface DraftActionListener {
class DraftsAdapter( class DraftsAdapter(
private val listener: DraftActionListener private val listener: DraftActionListener
) : PagedListAdapter<DraftEntity, BindingViewHolder<ItemDraftBinding>>( ) : PagedListAdapter<DraftEntity, BindingHolder<ItemDraftBinding>>(
object : DiffUtil.ItemCallback<DraftEntity>() { object : DiffUtil.ItemCallback<DraftEntity>() {
override fun areItemsTheSame(oldItem: DraftEntity, newItem: DraftEntity): Boolean { override fun areItemsTheSame(oldItem: DraftEntity, newItem: DraftEntity): Boolean {
return oldItem.id == newItem.id return oldItem.id == newItem.id
@ -47,15 +47,15 @@ class DraftsAdapter(
} }
) { ) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingViewHolder<ItemDraftBinding> { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemDraftBinding> {
val binding = ItemDraftBinding.inflate(LayoutInflater.from(parent.context), parent, false) 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.layoutManager = LinearLayoutManager(binding.root.context, RecyclerView.HORIZONTAL, false)
binding.draftMediaPreview.adapter = DraftMediaAdapter { binding.draftMediaPreview.adapter = DraftMediaAdapter {
getItem(viewHolder.adapterPosition)?.let { draft -> getItem(viewHolder.bindingAdapterPosition)?.let { draft ->
listener.onOpenDraft(draft) listener.onOpenDraft(draft)
} }
} }
@ -63,7 +63,7 @@ class DraftsAdapter(
return viewHolder return viewHolder
} }
override fun onBindViewHolder(holder: BindingViewHolder<ItemDraftBinding>, position: Int) { override fun onBindViewHolder(holder: BindingHolder<ItemDraftBinding>, position: Int) {
getItem(position)?.let { draft -> getItem(position)?.let { draft ->
holder.binding.root.setOnClickListener { holder.binding.root.setOnClickListener {
listener.onOpenDraft(draft) listener.onOpenDraft(draft)

View file

@ -4,10 +4,10 @@ import android.os.Bundle
import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.BaseActivity
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.instancemute.fragment.InstanceListFragment import com.keylesspalace.tusky.components.instancemute.fragment.InstanceListFragment
import com.keylesspalace.tusky.databinding.ActivityAccountListBinding
import dagger.android.DispatchingAndroidInjector import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector import dagger.android.HasAndroidInjector
import javax.inject.Inject import javax.inject.Inject
import kotlinx.android.synthetic.main.toolbar_basic.*
class InstanceListActivity: BaseActivity(), HasAndroidInjector { class InstanceListActivity: BaseActivity(), HasAndroidInjector {
@ -16,9 +16,10 @@ class InstanceListActivity: BaseActivity(), HasAndroidInjector {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val binding = ActivityAccountListBinding.inflate(layoutInflater)
setContentView(R.layout.activity_account_list) setContentView(R.layout.activity_account_list)
setSupportActionBar(toolbar) setSupportActionBar(binding.includedToolbar.toolbar)
supportActionBar?.apply { supportActionBar?.apply {
setTitle(R.string.title_domain_mutes) setTitle(R.string.title_domain_mutes)
setDisplayHomeAsUpEnabled(true) setDisplayHomeAsUpEnabled(true)

View file

@ -1,22 +1,31 @@
package com.keylesspalace.tusky.components.instancemute.adapter package com.keylesspalace.tusky.components.instancemute.adapter
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.instancemute.interfaces.InstanceActionListener 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<BindingHolder<ItemMutedDomainBinding>>() {
class DomainMutesAdapter(private val actionListener: InstanceActionListener): RecyclerView.Adapter<DomainMutesAdapter.ViewHolder>() {
var instances: MutableList<String> = mutableListOf() var instances: MutableList<String> = mutableListOf()
var bottomLoading: Boolean = false 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<ItemMutedDomainBinding> {
val binding = ItemMutedDomainBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return BindingHolder(binding)
} }
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: BindingHolder<ItemMutedDomainBinding>, position: Int) {
holder.setupWithInstance(instances[position]) val instance = instances[position]
holder.binding.mutedDomain.text = instance
holder.binding.mutedDomainUnmute.setOnClickListener {
actionListener.mute(false, instance, holder.bindingAdapterPosition)
}
} }
override fun getItemCount(): Int { override fun getItemCount(): Int {
@ -37,21 +46,10 @@ class DomainMutesAdapter(private val actionListener: InstanceActionListener): Re
notifyItemInserted(instances.size) notifyItemInserted(instances.size)
} }
fun removeItem(position: Int) fun removeItem(position: Int) {
{
if (position >= 0 && position < instances.size) { if (position >= 0 && position < instances.size) {
instances.removeAt(position) instances.removeAt(position)
notifyItemRemoved(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)
}
}
}
} }

View file

@ -2,9 +2,7 @@ package com.keylesspalace.tusky.components.instancemute.fragment
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.recyclerview.widget.DividerItemDecoration 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.R
import com.keylesspalace.tusky.components.instancemute.adapter.DomainMutesAdapter import com.keylesspalace.tusky.components.instancemute.adapter.DomainMutesAdapter
import com.keylesspalace.tusky.components.instancemute.interfaces.InstanceActionListener import com.keylesspalace.tusky.components.instancemute.interfaces.InstanceActionListener
import com.keylesspalace.tusky.databinding.FragmentInstanceListBinding
import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.HttpHeaderLink import com.keylesspalace.tusky.util.HttpHeaderLink
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.view.EndlessOnScrollListener import com.keylesspalace.tusky.view.EndlessOnScrollListener
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from
import com.uber.autodispose.autoDispose import com.uber.autodispose.autoDispose
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.synthetic.main.fragment_instance_list.*
import retrofit2.Call import retrofit2.Call
import retrofit2.Callback import retrofit2.Callback
import retrofit2.Response import retrofit2.Response
@ -31,9 +30,12 @@ import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectable, InstanceActionListener { class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectable, InstanceActionListener {
@Inject @Inject
lateinit var api: MastodonApi lateinit var api: MastodonApi
private val binding by viewBinding(FragmentInstanceListBinding::bind)
private var fetching = false private var fetching = false
private var bottomId: String? = null private var bottomId: String? = null
private var adapter = DomainMutesAdapter(this) 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?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
recyclerView.setHasFixedSize(true) binding.recyclerView.setHasFixedSize(true)
recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL)) binding.recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL))
recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
val layoutManager = LinearLayoutManager(view.context) val layoutManager = LinearLayoutManager(view.context)
recyclerView.layoutManager = layoutManager binding.recyclerView.layoutManager = layoutManager
scrollListener = object : EndlessOnScrollListener(layoutManager) { scrollListener = object : EndlessOnScrollListener(layoutManager) {
override fun onLoadMore(totalItemsCount: Int, view: RecyclerView) { 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() fetchInstances()
} }
@ -85,7 +87,7 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl
override fun onResponse(call: Call<Any>, response: Response<Any>) { override fun onResponse(call: Call<Any>, response: Response<Any>) {
if (response.isSuccessful) { if (response.isSuccessful) {
adapter.removeItem(position) 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) { .setAction(R.string.action_undo) {
mute(true, instance, position) mute(true, instance, position)
} }
@ -103,10 +105,10 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl
return return
} }
fetching = true fetching = true
instanceProgressBar.show() binding.instanceProgressBar.show()
if (id != null) { if (id != null) {
recyclerView.post { adapter.bottomLoading = true } binding.recyclerView.post { adapter.bottomLoading = true }
} }
api.domainBlocks(id, bottomId) api.domainBlocks(id, bottomId)
@ -116,7 +118,7 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl
val instances = response.body() val instances = response.body()
if (response.isSuccessful && instances != null) { if (response.isSuccessful && instances != null) {
onFetchInstancesSuccess(instances, response.headers().get("Link")) onFetchInstancesSuccess(instances, response.headers()["Link"])
} else { } else {
onFetchInstancesFailure(Exception(response.message())) onFetchInstancesFailure(Exception(response.message()))
} }
@ -127,7 +129,7 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl
private fun onFetchInstancesSuccess(instances: List<String>, linkHeader: String?) { private fun onFetchInstancesSuccess(instances: List<String>, linkHeader: String?) {
adapter.bottomLoading = false adapter.bottomLoading = false
instanceProgressBar.hide() binding.instanceProgressBar.hide()
val links = HttpHeaderLink.parse(linkHeader) val links = HttpHeaderLink.parse(linkHeader)
val next = HttpHeaderLink.findByRelationType(links, "next") val next = HttpHeaderLink.findByRelationType(links, "next")
@ -137,32 +139,32 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl
fetching = false fetching = false
if (adapter.itemCount == 0) { if (adapter.itemCount == 0) {
messageView.show() binding.messageView.show()
messageView.setup( binding.messageView.setup(
R.drawable.elephant_friend_empty, R.drawable.elephant_friend_empty,
R.string.message_empty, R.string.message_empty,
null null
) )
} else { } else {
messageView.hide() binding.messageView.hide()
} }
} }
private fun onFetchInstancesFailure(throwable: Throwable) { private fun onFetchInstancesFailure(throwable: Throwable) {
fetching = false fetching = false
instanceProgressBar.hide() binding.instanceProgressBar.hide()
Log.e(TAG, "Fetch failure", throwable) Log.e(TAG, "Fetch failure", throwable)
if (adapter.itemCount == 0) { if (adapter.itemCount == 0) {
messageView.show() binding.messageView.show()
if (throwable is IOException) { if (throwable is IOException) {
messageView.setup(R.drawable.elephant_offline, R.string.error_network) { binding.messageView.setup(R.drawable.elephant_offline, R.string.error_network) {
messageView.hide() binding.messageView.hide()
this.fetchInstances(null) this.fetchInstances(null)
} }
} else { } else {
messageView.setup(R.drawable.elephant_error, R.string.error_generic) { binding.messageView.setup(R.drawable.elephant_error, R.string.error_generic) {
messageView.hide() binding.messageView.hide()
this.fetchInstances(null) this.fetchInstances(null)
} }
} }

View file

@ -242,7 +242,7 @@ public class NotificationHelper {
if (currentNotifications.length() != 1) { if (currentNotifications.length() != 1) {
try { 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); String text = joinNames(context, currentNotifications);
summaryBuilder.setContentTitle(title) summaryBuilder.setContentTitle(title)
.setContentText(text); .setContentText(text);

View file

@ -8,14 +8,23 @@ import android.os.Build
import android.util.Log import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.* import android.widget.RadioButton
import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.SplashActivity 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
import com.keylesspalace.tusky.util.EmojiCompatFont.Companion.BLOBMOJI
import com.keylesspalace.tusky.util.EmojiCompatFont.Companion.FONTS 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.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposable
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -50,94 +59,85 @@ class EmojiPreference(
} }
override fun onClick() { override fun onClick() {
val view = LayoutInflater.from(context).inflate(R.layout.dialog_emojicompat, null) val binding = DialogEmojicompatBinding.inflate(LayoutInflater.from(context))
viewIds.forEachIndexed { index, viewId ->
setupItem(view.findViewById(viewId), FONTS[index]) setupItem(BLOBMOJI, binding.itemBlobmoji)
} setupItem(TWEMOJI, binding.itemTwemoji)
setupItem(NOTOEMOJI, binding.itemNotoemoji)
setupItem(SYSTEM_DEFAULT, binding.itemNomoji)
AlertDialog.Builder(context) AlertDialog.Builder(context)
.setView(view) .setView(binding.root)
.setPositiveButton(android.R.string.ok) { _, _ -> onDialogOk() } .setPositiveButton(android.R.string.ok) { _, _ -> onDialogOk() }
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
.show() .show()
} }
private fun setupItem(container: View, font: EmojiCompatFont) { private fun setupItem(font: EmojiCompatFont, binding: ItemEmojiPrefBinding) {
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)
// Initialize all the views // Initialize all the views
title.text = font.getDisplay(container.context) binding.emojiName.text = font.getDisplay(context)
caption.setText(font.caption) binding.emojiCaption.setText(font.caption)
thumb.setImageResource(font.img) 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 // There needs to be a list of all the radio buttons in order to uncheck them when one is selected
radioButtons.add(radio) radioButtons.add(binding.emojiRadioButton)
updateItem(font, container) updateItem(font, binding)
// Set actions // Set actions
download.setOnClickListener { startDownload(font, container) } binding.emojiDownload.setOnClickListener { startDownload(font, binding) }
cancel.setOnClickListener { cancelDownload(font, container) } binding.emojiDownloadCancel.setOnClickListener { cancelDownload(font, binding) }
radio.setOnClickListener { radioButton: View -> select(font, radioButton as RadioButton) } binding.emojiRadioButton.setOnClickListener { radioButton: View -> select(font, radioButton as RadioButton) }
container.setOnClickListener { containerView: View -> binding.root.setOnClickListener {
select(font, containerView.findViewById(R.id.emojicompat_radio)) select(font, binding.emojiRadioButton)
} }
} }
private fun startDownload(font: EmojiCompatFont, container: View) { private fun startDownload(font: EmojiCompatFont, binding: ItemEmojiPrefBinding) {
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)
// Switch to downloading style // Switch to downloading style
download.visibility = View.GONE binding.emojiDownload.hide()
caption.visibility = View.INVISIBLE binding.emojiCaption.visibility = View.INVISIBLE
progressBar.visibility = View.VISIBLE binding.emojiProgress.show()
progressBar.progress = 0 binding.emojiProgress.progress = 0
cancel.visibility = View.VISIBLE binding.emojiDownloadCancel.show()
font.downloadFontFile(context, okHttpClient) font.downloadFontFile(context, okHttpClient)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe( .subscribe(
{ progress -> { progress ->
// The progress is returned as a float between 0 and 1, or -1 if it could not determined // The progress is returned as a float between 0 and 1, or -1 if it could not determined
if (progress >= 0) { if (progress >= 0) {
progressBar.isIndeterminate = false binding.emojiProgress.isIndeterminate = false
val max = progressBar.max.toFloat() val max = binding.emojiProgress.max.toFloat()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
progressBar.setProgress((max * progress).toInt(), true) binding.emojiProgress.setProgress((max * progress).toInt(), true)
} else { } else {
progressBar.progress = (max * progress).toInt() binding.emojiProgress.progress = (max * progress).toInt()
} }
} else { } else {
progressBar.isIndeterminate = true binding.emojiProgress.isIndeterminate = true
} }
}, },
{ {
Toast.makeText(context, R.string.download_failed, Toast.LENGTH_SHORT).show() 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 } ).also { downloadDisposables[font.id] = it }
} }
private fun cancelDownload(font: EmojiCompatFont, container: View) { private fun cancelDownload(font: EmojiCompatFont, binding: ItemEmojiPrefBinding) {
font.deleteDownloadedFile(container.context) font.deleteDownloadedFile(context)
downloadDisposables[font.id]?.dispose() downloadDisposables[font.id]?.dispose()
downloadDisposables[font.id] = null downloadDisposables[font.id] = null
updateItem(font, container) updateItem(font, binding)
} }
private fun finishDownload(font: EmojiCompatFont, container: View) { private fun finishDownload(font: EmojiCompatFont, binding: ItemEmojiPrefBinding) {
select(font, container.findViewById(R.id.emojicompat_radio)) select(font, binding.emojiRadioButton)
updateItem(font, container) updateItem(font, binding)
// Set the flag to restart the app (because an update has been downloaded) // Set the flag to restart the app (because an update has been downloaded)
if (selected === original && currentNeedsUpdate) { if (selected === original && currentNeedsUpdate) {
updated = true updated = true
@ -153,54 +153,43 @@ class EmojiPreference(
*/ */
private fun select(font: EmojiCompatFont, radio: RadioButton) { private fun select(font: EmojiCompatFont, radio: RadioButton) {
selected = font selected = font
// Uncheck all the other buttons radioButtons.forEach { radioButton ->
for (other in radioButtons) { radioButton.isChecked = radioButton == radio
if (other !== radio) {
other.isChecked = false
}
} }
radio.isChecked = true
} }
/** /**
* Called when a "consistent" state is reached, i.e. it's not downloading the font * Called when a "consistent" state is reached, i.e. it's not downloading the font
* *
* @param font The font to be displayed * @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) { private fun updateItem(font: EmojiCompatFont, binding: ItemEmojiPrefBinding) {
// 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)
// There's no download going on // There's no download going on
progress.visibility = View.GONE binding.emojiProgress.hide()
cancel.visibility = View.GONE binding.emojiDownloadCancel.hide()
caption.visibility = View.VISIBLE binding.emojiCaption.show()
if (font.isDownloaded(context)) { if (font.isDownloaded(context)) {
// Make it selectable // Make it selectable
download.visibility = View.GONE binding.emojiDownload.hide()
radio.visibility = View.VISIBLE binding.emojiRadioButton.show()
container.isClickable = true binding.root.isClickable = true
} else { } else {
// Make it downloadable // Make it downloadable
download.visibility = View.VISIBLE binding.emojiDownload.show()
radio.visibility = View.GONE binding.emojiRadioButton.hide()
container.isClickable = false binding.root.isClickable = false
} }
// Select it if necessary // Select it if necessary
if (font === selected) { if (font === selected) {
radio.isChecked = true binding.emojiRadioButton.isChecked = true
// Update available // Update available
if (!font.isDownloaded(context)) { if (!font.isDownloaded(context)) {
currentNeedsUpdate = true currentNeedsUpdate = true
} }
} else { } else {
radio.isChecked = false binding.emojiRadioButton.isChecked = false
} }
} }
@ -246,13 +235,5 @@ class EmojiPreference(
companion object { companion object {
private const val TAG = "EmojiPreference" 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
)
} }
} }

View file

@ -28,12 +28,12 @@ import com.keylesspalace.tusky.MainActivity
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
import com.keylesspalace.tusky.databinding.ActivityPreferencesBinding
import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.ThemeUtils import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.getNonNullString import com.keylesspalace.tusky.util.getNonNullString
import dagger.android.DispatchingAndroidInjector import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector import dagger.android.HasAndroidInjector
import kotlinx.android.synthetic.main.toolbar_basic.*
import javax.inject.Inject import javax.inject.Inject
class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreferenceChangeListener, class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreferenceChangeListener,
@ -48,12 +48,12 @@ class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreference
private var restartActivitiesOnExit: Boolean = false private var restartActivitiesOnExit: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_preferences) val binding = ActivityPreferencesBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(toolbar) setSupportActionBar(binding.includedToolbar.toolbar)
supportActionBar?.run { supportActionBar?.run {
setDisplayHomeAsUpEnabled(true) setDisplayHomeAsUpEnabled(true)
setDisplayShowHomeEnabled(true) setDisplayShowHomeEnabled(true)

View file

@ -22,11 +22,11 @@ import androidx.activity.viewModels
import com.keylesspalace.tusky.BottomSheetActivity import com.keylesspalace.tusky.BottomSheetActivity
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.report.adapter.ReportPagerAdapter import com.keylesspalace.tusky.components.report.adapter.ReportPagerAdapter
import com.keylesspalace.tusky.databinding.ActivityReportBinding
import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.util.viewBinding
import dagger.android.DispatchingAndroidInjector import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector import dagger.android.HasAndroidInjector
import kotlinx.android.synthetic.main.activity_report.*
import kotlinx.android.synthetic.main.toolbar_basic.*
import javax.inject.Inject import javax.inject.Inject
class ReportActivity : BottomSheetActivity(), HasAndroidInjector { class ReportActivity : BottomSheetActivity(), HasAndroidInjector {
@ -39,6 +39,8 @@ class ReportActivity : BottomSheetActivity(), HasAndroidInjector {
private val viewModel: ReportViewModel by viewModels { viewModelFactory } private val viewModel: ReportViewModel by viewModels { viewModelFactory }
private val binding by viewBinding(ActivityReportBinding::inflate)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val accountId = intent?.getStringExtra(ACCOUNT_ID) val accountId = intent?.getStringExtra(ACCOUNT_ID)
@ -50,9 +52,9 @@ class ReportActivity : BottomSheetActivity(), HasAndroidInjector {
viewModel.init(accountId, accountUserName, intent?.getStringExtra(STATUS_ID)) viewModel.init(accountId, accountUserName, intent?.getStringExtra(STATUS_ID))
setContentView(R.layout.activity_report) setContentView(binding.root)
setSupportActionBar(toolbar) setSupportActionBar(binding.includedToolbar.toolbar)
supportActionBar?.apply { supportActionBar?.apply {
title = getString(R.string.report_username_format, viewModel.accountUserName) title = getString(R.string.report_username_format, viewModel.accountUserName)
@ -69,8 +71,8 @@ class ReportActivity : BottomSheetActivity(), HasAndroidInjector {
} }
private fun initViewPager() { private fun initViewPager() {
wizard.isUserInputEnabled = false binding.wizard.isUserInputEnabled = false
wizard.adapter = ReportPagerAdapter(this) binding.wizard.adapter = ReportPagerAdapter(this)
} }
private fun subscribeObservables() { private fun subscribeObservables() {
@ -96,18 +98,18 @@ class ReportActivity : BottomSheetActivity(), HasAndroidInjector {
} }
private fun showPreviousScreen() { private fun showPreviousScreen() {
when (wizard.currentItem) { when (binding.wizard.currentItem) {
0 -> closeScreen() 0 -> closeScreen()
1 -> showStatusesPage() 1 -> showStatusesPage()
} }
} }
private fun showDonePage() { private fun showDonePage() {
wizard.currentItem = 2 binding.wizard.currentItem = 2
} }
private fun showNotesPage() { private fun showNotesPage() {
wizard.currentItem = 1 binding.wizard.currentItem = 1
} }
private fun closeScreen() { private fun closeScreen() {
@ -115,7 +117,7 @@ class ReportActivity : BottomSheetActivity(), HasAndroidInjector {
} }
private fun showStatusesPage() { private fun showStatusesPage() {
wizard.currentItem = 0 binding.wizard.currentItem = 0
} }
companion object { companion object {

View file

@ -21,6 +21,7 @@ import android.view.View
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.report.model.StatusViewState 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.Emoji
import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.interfaces.LinkListener 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.COLLAPSE_INPUT_FILTER
import com.keylesspalace.tusky.util.StatusViewHelper.Companion.NO_INPUT_FILTER import com.keylesspalace.tusky.util.StatusViewHelper.Companion.NO_INPUT_FILTER
import com.keylesspalace.tusky.viewdata.toViewData import com.keylesspalace.tusky.viewdata.toViewData
import kotlinx.android.synthetic.main.item_report_status.view.*
import java.util.* import java.util.*
class StatusViewHolder( class StatusViewHolder(
itemView: View, private val binding: ItemReportStatusBinding,
private val statusDisplayOptions: StatusDisplayOptions, private val statusDisplayOptions: StatusDisplayOptions,
private val viewState: StatusViewState, private val viewState: StatusViewState,
private val adapterHandler: AdapterHandler, private val adapterHandler: AdapterHandler,
private val getStatusForPosition: (Int) -> Status? 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 mediaViewHeight = itemView.context.resources.getDimensionPixelSize(R.dimen.status_media_preview_height)
private val statusViewHelper = StatusViewHelper(itemView) private val statusViewHelper = StatusViewHelper(itemView)
@ -56,16 +56,16 @@ class StatusViewHolder(
} }
init { init {
itemView.statusSelection.setOnCheckedChangeListener { _, isChecked -> binding.statusSelection.setOnCheckedChangeListener { _, isChecked ->
status()?.let { status -> status()?.let { status ->
adapterHandler.setStatusChecked(status, isChecked) adapterHandler.setStatusChecked(status, isChecked)
} }
} }
itemView.status_media_preview_container.clipToOutline = true binding.statusMediaPreviewContainer.clipToOutline = true
} }
fun bind(status: Status) { fun bind(status: Status) {
itemView.statusSelection.isChecked = adapterHandler.isStatusChecked(status.id) binding.statusSelection.isChecked = adapterHandler.isStatusChecked(status.id)
updateTextView() updateTextView()
@ -86,18 +86,18 @@ class StatusViewHolder(
if (status.spoilerText.isBlank()) { if (status.spoilerText.isBlank()) {
setTextVisible(true, status.content, status.mentions, status.emojis, adapterHandler) setTextVisible(true, status.content, status.mentions, status.emojis, adapterHandler)
itemView.statusContentWarningButton.hide() binding.statusContentWarningButton.hide()
itemView.statusContentWarningDescription.hide() binding.statusContentWarningDescription.hide()
} else { } else {
val emojiSpoiler = status.spoilerText.emojify(status.emojis, itemView.statusContentWarningDescription, statusDisplayOptions.animateEmojis) val emojiSpoiler = status.spoilerText.emojify(status.emojis, binding.statusContentWarningDescription, statusDisplayOptions.animateEmojis)
itemView.statusContentWarningDescription.text = emojiSpoiler binding.statusContentWarningDescription.text = emojiSpoiler
itemView.statusContentWarningDescription.show() binding.statusContentWarningDescription.show()
itemView.statusContentWarningButton.show() binding.statusContentWarningButton.show()
setContentWarningButtonText(viewState.isContentShow(status.id, true)) setContentWarningButtonText(viewState.isContentShow(status.id, true))
itemView.statusContentWarningButton.setOnClickListener { binding.statusContentWarningButton.setOnClickListener {
status()?.let { status -> status()?.let { status ->
val contentShown = viewState.isContentShow(status.id, true) val contentShown = viewState.isContentShow(status.id, true)
itemView.statusContentWarningDescription.invalidate() binding.statusContentWarningDescription.invalidate()
viewState.setContentShow(status.id, !contentShown) viewState.setContentShow(status.id, !contentShown)
setTextVisible(!contentShown, status.content, status.mentions, status.emojis, adapterHandler) setTextVisible(!contentShown, status.content, status.mentions, status.emojis, adapterHandler)
setContentWarningButtonText(!contentShown) setContentWarningButtonText(!contentShown)
@ -110,9 +110,9 @@ class StatusViewHolder(
private fun setContentWarningButtonText(contentShown: Boolean) { private fun setContentWarningButtonText(contentShown: Boolean) {
if(contentShown) { if(contentShown) {
itemView.statusContentWarningButton.setText(R.string.status_content_warning_show_less) binding.statusContentWarningButton.setText(R.string.status_content_warning_show_less)
} else { } 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<Emoji>, emojis: List<Emoji>,
listener: LinkListener) { listener: LinkListener) {
if (expanded) { if (expanded) {
val emojifiedText = content.emojify(emojis, itemView.statusContent, statusDisplayOptions.animateEmojis) val emojifiedText = content.emojify(emojis, binding.statusContent, statusDisplayOptions.animateEmojis)
LinkHelper.setClickableText(itemView.statusContent, emojifiedText, mentions, listener) LinkHelper.setClickableText(binding.statusContent, emojifiedText, mentions, listener)
} else { } else {
LinkHelper.setClickableMentions(itemView.statusContent, mentions, listener) LinkHelper.setClickableMentions(binding.statusContent, mentions, listener)
} }
if (itemView.statusContent.text.isNullOrBlank()) { if (binding.statusContent.text.isNullOrBlank()) {
itemView.statusContent.hide() binding.statusContent.hide()
} else { } else {
itemView.statusContent.show() binding.statusContent.show()
} }
} }
private fun setCreatedAt(createdAt: Date?) { private fun setCreatedAt(createdAt: Date?) {
if (statusDisplayOptions.useAbsoluteTime) { if (statusDisplayOptions.useAbsoluteTime) {
itemView.timestampInfo.text = statusViewHelper.getAbsoluteTime(createdAt) binding.timestampInfo.text = statusViewHelper.getAbsoluteTime(createdAt)
} else { } else {
itemView.timestampInfo.text = if (createdAt != null) { binding.timestampInfo.text = if (createdAt != null) {
val then = createdAt.time val then = createdAt.time
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
TimestampUtils.getRelativeTimeSpanString(itemView.timestampInfo.context, then, now) TimestampUtils.getRelativeTimeSpanString(binding.timestampInfo.context, then, now)
} else { } else {
// unknown minutes~ // unknown minutes~
"?m" "?m"
@ -149,30 +149,29 @@ class StatusViewHolder(
} }
} }
private fun setupCollapsedState(collapsible: Boolean, collapsed: Boolean, expanded: Boolean, spoilerText: String) { private fun setupCollapsedState(collapsible: Boolean, collapsed: Boolean, expanded: Boolean, spoilerText: String) {
/* input filter for TextViews have to be set before text */ /* input filter for TextViews have to be set before text */
if (collapsible && (expanded || TextUtils.isEmpty(spoilerText))) { if (collapsible && (expanded || TextUtils.isEmpty(spoilerText))) {
itemView.buttonToggleContent.setOnClickListener{ binding.buttonToggleContent.setOnClickListener{
status()?.let { status -> status()?.let { status ->
viewState.setCollapsed(status.id, !collapsed) viewState.setCollapsed(status.id, !collapsed)
updateTextView() updateTextView()
} }
} }
itemView.buttonToggleContent.show() binding.buttonToggleContent.show()
if (collapsed) { if (collapsed) {
itemView.buttonToggleContent.setText(R.string.status_content_show_more) binding.buttonToggleContent.setText(R.string.status_content_show_more)
itemView.statusContent.filters = COLLAPSE_INPUT_FILTER binding.statusContent.filters = COLLAPSE_INPUT_FILTER
} else { } else {
itemView.buttonToggleContent.setText(R.string.status_content_show_less) binding.buttonToggleContent.setText(R.string.status_content_show_less)
itemView.statusContent.filters = NO_INPUT_FILTER binding.statusContent.filters = NO_INPUT_FILTER
} }
} else { } else {
itemView.buttonToggleContent.hide() binding.buttonToggleContent.hide()
itemView.statusContent.filters = NO_INPUT_FILTER binding.statusContent.filters = NO_INPUT_FILTER
} }
} }
private fun status() = getStatusForPosition(adapterPosition) private fun status() = getStatusForPosition(bindingAdapterPosition)
} }

View file

@ -20,8 +20,8 @@ import android.view.ViewGroup
import androidx.paging.PagedListAdapter import androidx.paging.PagedListAdapter
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.report.model.StatusViewState import com.keylesspalace.tusky.components.report.model.StatusViewState
import com.keylesspalace.tusky.databinding.ItemReportStatusBinding
import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.util.StatusDisplayOptions import com.keylesspalace.tusky.util.StatusDisplayOptions
@ -29,29 +29,25 @@ class StatusesAdapter(
private val statusDisplayOptions: StatusDisplayOptions, private val statusDisplayOptions: StatusDisplayOptions,
private val statusViewState: StatusViewState, private val statusViewState: StatusViewState,
private val adapterHandler: AdapterHandler private val adapterHandler: AdapterHandler
) : PagedListAdapter<Status, RecyclerView.ViewHolder>(STATUS_COMPARATOR) { ) : PagedListAdapter<Status, StatusViewHolder>(STATUS_COMPARATOR) {
private val statusForPosition: (Int) -> Status? = { position: Int -> private val statusForPosition: (Int) -> Status? = { position: Int ->
if (position != RecyclerView.NO_POSITION) getItem(position) else null if (position != RecyclerView.NO_POSITION) getItem(position) else null
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StatusViewHolder {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val binding = ItemReportStatusBinding.inflate(LayoutInflater.from(parent.context), parent, false)
val view = LayoutInflater.from(parent.context) return StatusViewHolder(binding, statusDisplayOptions, statusViewState, adapterHandler,
.inflate(R.layout.item_report_status, parent, false)
return StatusViewHolder(view, statusDisplayOptions, statusViewState, adapterHandler,
statusForPosition) statusForPosition)
} }
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onBindViewHolder(holder: StatusViewHolder, position: Int) {
getItem(position)?.let { status -> getItem(position)?.let { status ->
(holder as? StatusViewHolder)?.bind(status) holder.bind(status)
} }
} }
companion object { companion object {
val STATUS_COMPARATOR = object : DiffUtil.ItemCallback<Status>() { val STATUS_COMPARATOR = object : DiffUtil.ItemCallback<Status>() {
override fun areContentsTheSame(oldItem: Status, newItem: Status): Boolean = override fun areContentsTheSame(oldItem: Status, newItem: Status): Boolean =
oldItem == newItem oldItem == newItem
@ -59,7 +55,5 @@ class StatusesAdapter(
override fun areItemsTheSame(oldItem: Status, newItem: Status): Boolean = override fun areItemsTheSame(oldItem: Status, newItem: Status): Boolean =
oldItem.id == newItem.id oldItem.id == newItem.id
} }
} }
} }

View file

@ -22,12 +22,13 @@ import androidx.fragment.app.activityViewModels
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.report.ReportViewModel import com.keylesspalace.tusky.components.report.ReportViewModel
import com.keylesspalace.tusky.components.report.Screen import com.keylesspalace.tusky.components.report.Screen
import com.keylesspalace.tusky.databinding.FragmentReportDoneBinding
import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.util.Loading import com.keylesspalace.tusky.util.Loading
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.show
import kotlinx.android.synthetic.main.fragment_report_done.* import com.keylesspalace.tusky.util.viewBinding
import javax.inject.Inject import javax.inject.Inject
class ReportDoneFragment : Fragment(R.layout.fragment_report_done), Injectable { 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 viewModel: ReportViewModel by activityViewModels { viewModelFactory }
private val binding by viewBinding(FragmentReportDoneBinding::bind)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 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() handleClicks()
subscribeObservables() subscribeObservables()
} }
@ -46,14 +49,14 @@ class ReportDoneFragment : Fragment(R.layout.fragment_report_done), Injectable {
private fun subscribeObservables() { private fun subscribeObservables() {
viewModel.muteState.observe(viewLifecycleOwner) { viewModel.muteState.observe(viewLifecycleOwner) {
if (it !is Loading) { if (it !is Loading) {
buttonMute.show() binding.buttonMute.show()
progressMute.show() binding.progressMute.show()
} else { } else {
buttonMute.hide() binding.buttonMute.hide()
progressMute.hide() binding.progressMute.hide()
} }
buttonMute.setText(when (it.data) { binding.buttonMute.setText(when (it.data) {
true -> R.string.action_unmute true -> R.string.action_unmute
else -> R.string.action_mute else -> R.string.action_mute
}) })
@ -61,14 +64,14 @@ class ReportDoneFragment : Fragment(R.layout.fragment_report_done), Injectable {
viewModel.blockState.observe(viewLifecycleOwner) { viewModel.blockState.observe(viewLifecycleOwner) {
if (it !is Loading) { if (it !is Loading) {
buttonBlock.show() binding.buttonBlock.show()
progressBlock.show() binding.progressBlock.show()
} }
else{ else {
buttonBlock.hide() binding.buttonBlock.hide()
progressBlock.hide() binding.progressBlock.hide()
} }
buttonBlock.setText(when (it.data) { binding.buttonBlock.setText(when (it.data) {
true -> R.string.action_unblock true -> R.string.action_unblock
else -> R.string.action_block else -> R.string.action_block
}) })
@ -77,13 +80,13 @@ class ReportDoneFragment : Fragment(R.layout.fragment_report_done), Injectable {
} }
private fun handleClicks() { private fun handleClicks() {
buttonDone.setOnClickListener { binding.buttonDone.setOnClickListener {
viewModel.navigateTo(Screen.Finish) viewModel.navigateTo(Screen.Finish)
} }
buttonBlock.setOnClickListener { binding.buttonBlock.setOnClickListener {
viewModel.toggleBlock() viewModel.toggleBlock()
} }
buttonMute.setOnClickListener { binding.buttonMute.setOnClickListener {
viewModel.toggleMute() viewModel.toggleMute()
} }
} }
@ -91,5 +94,4 @@ class ReportDoneFragment : Fragment(R.layout.fragment_report_done), Injectable {
companion object { companion object {
fun newInstance() = ReportDoneFragment() fun newInstance() = ReportDoneFragment()
} }
} }

View file

@ -24,10 +24,10 @@ import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.report.ReportViewModel import com.keylesspalace.tusky.components.report.ReportViewModel
import com.keylesspalace.tusky.components.report.Screen import com.keylesspalace.tusky.components.report.Screen
import com.keylesspalace.tusky.databinding.FragmentReportNoteBinding
import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.util.* import com.keylesspalace.tusky.util.*
import kotlinx.android.synthetic.main.fragment_report_note.*
import java.io.IOException import java.io.IOException
import javax.inject.Inject 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 viewModel: ReportViewModel by activityViewModels { viewModelFactory }
private val binding by viewBinding(FragmentReportNoteBinding::bind)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
fillViews() fillViews()
handleChanges() handleChanges()
@ -46,29 +48,29 @@ class ReportNoteFragment : Fragment(R.layout.fragment_report_note), Injectable {
} }
private fun handleChanges() { private fun handleChanges() {
editNote.doAfterTextChanged { binding.editNote.doAfterTextChanged {
viewModel.reportNote = it?.toString() ?: "" viewModel.reportNote = it?.toString() ?: ""
} }
checkIsNotifyRemote.setOnCheckedChangeListener { _, isChecked -> binding.checkIsNotifyRemote.setOnCheckedChangeListener { _, isChecked ->
viewModel.isRemoteNotify = isChecked viewModel.isRemoteNotify = isChecked
} }
} }
private fun fillViews() { private fun fillViews() {
editNote.setText(viewModel.reportNote) binding.editNote.setText(viewModel.reportNote)
if (viewModel.isRemoteAccount){ if (viewModel.isRemoteAccount){
checkIsNotifyRemote.show() binding.checkIsNotifyRemote.show()
reportDescriptionRemoteInstance.show() binding.reportDescriptionRemoteInstance.show()
} }
else{ else{
checkIsNotifyRemote.hide() binding.checkIsNotifyRemote.hide()
reportDescriptionRemoteInstance.hide() binding.reportDescriptionRemoteInstance.hide()
} }
if (viewModel.isRemoteAccount) if (viewModel.isRemoteAccount)
checkIsNotifyRemote.text = getString(R.string.report_remote_instance, viewModel.remoteServer) binding.checkIsNotifyRemote.text = getString(R.string.report_remote_instance, viewModel.remoteServer)
checkIsNotifyRemote.isChecked = viewModel.isRemoteNotify binding.checkIsNotifyRemote.isChecked = viewModel.isRemoteNotify
} }
private fun subscribeObservables() { private fun subscribeObservables() {
@ -83,13 +85,13 @@ class ReportNoteFragment : Fragment(R.layout.fragment_report_note), Injectable {
} }
private fun showError(error: Throwable?) { private fun showError(error: Throwable?) {
editNote.isEnabled = true binding.editNote.isEnabled = true
checkIsNotifyRemote.isEnabled = true binding.checkIsNotifyRemote.isEnabled = true
buttonReport.isEnabled = true binding.buttonReport.isEnabled = true
buttonBack.isEnabled = true binding.buttonBack.isEnabled = true
progressBar.hide() 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 { .apply {
setAction(R.string.action_retry) { setAction(R.string.action_retry) {
sendReport() sendReport()
@ -103,19 +105,19 @@ class ReportNoteFragment : Fragment(R.layout.fragment_report_note), Injectable {
} }
private fun showLoading() { private fun showLoading() {
buttonReport.isEnabled = false binding.buttonReport.isEnabled = false
buttonBack.isEnabled = false binding.buttonBack.isEnabled = false
editNote.isEnabled = false binding.editNote.isEnabled = false
checkIsNotifyRemote.isEnabled = false binding.checkIsNotifyRemote.isEnabled = false
progressBar.show() binding.progressBar.show()
} }
private fun handleClicks() { private fun handleClicks() {
buttonBack.setOnClickListener { binding.buttonBack.setOnClickListener {
viewModel.navigateTo(Screen.Back) viewModel.navigateTo(Screen.Back)
} }
buttonReport.setOnClickListener { binding.buttonReport.setOnClickListener {
sendReport() sendReport()
} }
} }
@ -123,5 +125,4 @@ class ReportNoteFragment : Fragment(R.layout.fragment_report_note), Injectable {
companion object { companion object {
fun newInstance() = ReportNoteFragment() fun newInstance() = ReportNoteFragment()
} }
} }

View file

@ -34,6 +34,7 @@ import com.keylesspalace.tusky.components.report.ReportViewModel
import com.keylesspalace.tusky.components.report.Screen import com.keylesspalace.tusky.components.report.Screen
import com.keylesspalace.tusky.components.report.adapter.AdapterHandler import com.keylesspalace.tusky.components.report.adapter.AdapterHandler
import com.keylesspalace.tusky.components.report.adapter.StatusesAdapter import com.keylesspalace.tusky.components.report.adapter.StatusesAdapter
import com.keylesspalace.tusky.databinding.FragmentReportStatusesBinding
import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory 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.StatusDisplayOptions
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.viewdata.AttachmentViewData import com.keylesspalace.tusky.viewdata.AttachmentViewData
import kotlinx.android.synthetic.main.fragment_report_statuses.*
import javax.inject.Inject import javax.inject.Inject
class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Injectable, AdapterHandler { 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 viewModel: ReportViewModel by activityViewModels { viewModelFactory }
private val binding by viewBinding(FragmentReportStatusesBinding::bind)
private lateinit var adapter: StatusesAdapter private lateinit var adapter: StatusesAdapter
private var snackbarErrorRetry: Snackbar? = null private var snackbarErrorRetry: Snackbar? = null
@ -93,9 +96,9 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje
} }
private fun setupSwipeRefreshLayout() { private fun setupSwipeRefreshLayout() {
swipeRefreshLayout.setColorSchemeResources(R.color.chinwag_green) binding.swipeRefreshLayout.setColorSchemeResources(R.color.chinwag_green)
swipeRefreshLayout.setOnRefreshListener { binding.swipeRefreshLayout.setOnRefreshListener {
snackbarErrorRetry?.dismiss() snackbarErrorRetry?.dismiss()
viewModel.refreshStatuses() viewModel.refreshStatuses()
} }
@ -118,10 +121,10 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje
adapter = StatusesAdapter(statusDisplayOptions, adapter = StatusesAdapter(statusDisplayOptions,
viewModel.statusViewState, this) viewModel.statusViewState, this)
recyclerView.addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL)) binding.recyclerView.addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL))
recyclerView.layoutManager = LinearLayoutManager(requireContext()) binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
(recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false (binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
viewModel.statuses.observe(viewLifecycleOwner) { viewModel.statuses.observe(viewLifecycleOwner) {
adapter.submitList(it) adapter.submitList(it)
@ -129,9 +132,9 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje
viewModel.networkStateAfter.observe(viewLifecycleOwner) { viewModel.networkStateAfter.observe(viewLifecycleOwner) {
if (it?.status == com.keylesspalace.tusky.util.Status.RUNNING) if (it?.status == com.keylesspalace.tusky.util.Status.RUNNING)
progressBarBottom.show() binding.progressBarBottom.show()
else else
progressBarBottom.hide() binding.progressBarBottom.hide()
if (it?.status == com.keylesspalace.tusky.util.Status.FAILED) if (it?.status == com.keylesspalace.tusky.util.Status.FAILED)
showError(it.msg) showError(it.msg)
@ -139,22 +142,22 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje
viewModel.networkStateBefore.observe(viewLifecycleOwner) { viewModel.networkStateBefore.observe(viewLifecycleOwner) {
if (it?.status == com.keylesspalace.tusky.util.Status.RUNNING) if (it?.status == com.keylesspalace.tusky.util.Status.RUNNING)
progressBarTop.show() binding.progressBarTop.show()
else else
progressBarTop.hide() binding.progressBarTop.hide()
if (it?.status == com.keylesspalace.tusky.util.Status.FAILED) if (it?.status == com.keylesspalace.tusky.util.Status.FAILED)
showError(it.msg) showError(it.msg)
} }
viewModel.networkStateRefresh.observe(viewLifecycleOwner) { viewModel.networkStateRefresh.observe(viewLifecycleOwner) {
if (it?.status == com.keylesspalace.tusky.util.Status.RUNNING && !swipeRefreshLayout.isRefreshing) if (it?.status == com.keylesspalace.tusky.util.Status.RUNNING && !binding.swipeRefreshLayout.isRefreshing)
progressBarLoading.show() binding.progressBarLoading.show()
else else
progressBarLoading.hide() binding.progressBarLoading.hide()
if (it?.status != com.keylesspalace.tusky.util.Status.RUNNING) 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) if (it?.status == com.keylesspalace.tusky.util.Status.FAILED)
showError(it.msg) showError(it.msg)
} }
@ -162,7 +165,7 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje
private fun showError(@Suppress("UNUSED_PARAMETER") msg: String?) { private fun showError(@Suppress("UNUSED_PARAMETER") msg: String?) {
if (snackbarErrorRetry?.isShown != true) { 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) { snackbarErrorRetry?.setAction(R.string.action_retry) {
viewModel.retryStatusLoad() viewModel.retryStatusLoad()
} }
@ -172,11 +175,11 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje
private fun handleClicks() { private fun handleClicks() {
buttonCancel.setOnClickListener { binding.buttonCancel.setOnClickListener {
viewModel.navigateTo(Screen.Back) viewModel.navigateTo(Screen.Back)
} }
buttonContinue.setOnClickListener { binding.buttonContinue.setOnClickListener {
viewModel.navigateTo(Screen.Note) viewModel.navigateTo(Screen.Note)
} }
} }

View file

@ -18,20 +18,19 @@ package com.keylesspalace.tusky.components.scheduled
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.lifecycle.ViewModelProvider import androidx.activity.viewModels
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.BaseActivity
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.compose.ComposeActivity import com.keylesspalace.tusky.components.compose.ComposeActivity
import com.keylesspalace.tusky.databinding.ActivityScheduledTootBinding
import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.entity.ScheduledStatus import com.keylesspalace.tusky.entity.ScheduledStatus
import com.keylesspalace.tusky.util.Status import com.keylesspalace.tusky.util.Status
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show 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 import javax.inject.Inject
class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injectable { class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injectable {
@ -39,31 +38,31 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec
@Inject @Inject
lateinit var viewModelFactory: ViewModelFactory lateinit var viewModelFactory: ViewModelFactory
lateinit var viewModel: ScheduledTootViewModel private val viewModel: ScheduledTootViewModel by viewModels { viewModelFactory }
private val adapter = ScheduledTootAdapter(this) private val adapter = ScheduledTootAdapter(this)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) 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 { supportActionBar?.run {
title = getString(R.string.title_scheduled_toot) title = getString(R.string.title_scheduled_toot)
setDisplayHomeAsUpEnabled(true) setDisplayHomeAsUpEnabled(true)
setDisplayShowHomeEnabled(true) setDisplayShowHomeEnabled(true)
} }
swipeRefreshLayout.setOnRefreshListener(this::refreshStatuses) binding.swipeRefreshLayout.setOnRefreshListener(this::refreshStatuses)
swipeRefreshLayout.setColorSchemeResources(R.color.chinwag_green) binding.swipeRefreshLayout.setColorSchemeResources(R.color.chinwag_green)
scheduledTootList.setHasFixedSize(true) binding.scheduledTootList.setHasFixedSize(true)
scheduledTootList.layoutManager = LinearLayoutManager(this) binding.scheduledTootList.layoutManager = LinearLayoutManager(this)
val divider = DividerItemDecoration(this, DividerItemDecoration.VERTICAL) val divider = DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
scheduledTootList.addItemDecoration(divider) binding.scheduledTootList.addItemDecoration(divider)
scheduledTootList.adapter = adapter binding.scheduledTootList.adapter = adapter
viewModel = ViewModelProvider(this, viewModelFactory)[ScheduledTootViewModel::class.java]
viewModel.data.observe(this) { viewModel.data.observe(this) {
adapter.submitList(it) adapter.submitList(it)
@ -72,31 +71,31 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec
viewModel.networkState.observe(this) { (status) -> viewModel.networkState.observe(this) { (status) ->
when(status) { when(status) {
Status.SUCCESS -> { Status.SUCCESS -> {
progressBar.hide() binding.progressBar.hide()
swipeRefreshLayout.isRefreshing = false binding.swipeRefreshLayout.isRefreshing = false
if(viewModel.data.value?.loadedCount == 0) { if(viewModel.data.value?.loadedCount == 0) {
errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_scheduled_status) binding.errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_scheduled_status)
errorMessageView.show() binding.errorMessageView.show()
} else { } else {
errorMessageView.hide() binding.errorMessageView.hide()
} }
} }
Status.RUNNING -> { Status.RUNNING -> {
errorMessageView.hide() binding.errorMessageView.hide()
if(viewModel.data.value?.loadedCount ?: 0 > 0) { if(viewModel.data.value?.loadedCount ?: 0 > 0) {
swipeRefreshLayout.isRefreshing = true binding.swipeRefreshLayout.isRefreshing = true
} else { } else {
progressBar.show() binding.progressBar.show()
} }
} }
Status.FAILED -> { Status.FAILED -> {
if(viewModel.data.value?.loadedCount ?: 0 >= 0) { if(viewModel.data.value?.loadedCount ?: 0 >= 0) {
progressBar.hide() binding.progressBar.hide()
swipeRefreshLayout.isRefreshing = false binding.swipeRefreshLayout.isRefreshing = false
errorMessageView.setup(R.drawable.elephant_error, R.string.error_generic) { binding.errorMessageView.setup(R.drawable.elephant_error, R.string.error_generic) {
refreshStatuses() refreshStatuses()
} }
errorMessageView.show() binding.errorMessageView.show()
} }
} }
} }

View file

@ -18,13 +18,11 @@ package com.keylesspalace.tusky.components.scheduled
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageButton
import android.widget.TextView
import androidx.paging.PagedListAdapter import androidx.paging.PagedListAdapter
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import com.keylesspalace.tusky.databinding.ItemScheduledTootBinding
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.entity.ScheduledStatus import com.keylesspalace.tusky.entity.ScheduledStatus
import com.keylesspalace.tusky.util.BindingHolder
interface ScheduledTootActionListener { interface ScheduledTootActionListener {
fun edit(item: ScheduledStatus) fun edit(item: ScheduledStatus)
@ -33,7 +31,7 @@ interface ScheduledTootActionListener {
class ScheduledTootAdapter( class ScheduledTootAdapter(
val listener: ScheduledTootActionListener val listener: ScheduledTootActionListener
) : PagedListAdapter<ScheduledStatus, ScheduledTootAdapter.TootViewHolder>( ) : PagedListAdapter<ScheduledStatus, BindingHolder<ItemScheduledTootBinding>>(
object: DiffUtil.ItemCallback<ScheduledStatus>(){ object: DiffUtil.ItemCallback<ScheduledStatus>(){
override fun areItemsTheSame(oldItem: ScheduledStatus, newItem: ScheduledStatus): Boolean { override fun areItemsTheSame(oldItem: ScheduledStatus, newItem: ScheduledStatus): Boolean {
return oldItem.id == newItem.id return oldItem.id == newItem.id
@ -46,40 +44,24 @@ class ScheduledTootAdapter(
} }
) { ) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TootViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemScheduledTootBinding> {
val view = LayoutInflater.from(parent.context) val binding = ItemScheduledTootBinding.inflate(LayoutInflater.from(parent.context), parent, false)
.inflate(R.layout.item_scheduled_toot, parent, false) return BindingHolder(binding)
return TootViewHolder(view)
} }
override fun onBindViewHolder(viewHolder: TootViewHolder, position: Int) { override fun onBindViewHolder(holder: BindingHolder<ItemScheduledTootBinding>, position: Int) {
getItem(position)?.let{ getItem(position)?.let{ item ->
viewHolder.bind(it) holder.binding.edit.isEnabled = true
} holder.binding.delete.isEnabled = true
} holder.binding.text.text = item.params.text
holder.binding.edit.setOnClickListener { v: View ->
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 ->
v.isEnabled = false v.isEnabled = false
listener.edit(item) listener.edit(item)
} }
delete.setOnClickListener { v: View -> holder.binding.delete.setOnClickListener { v: View ->
v.isEnabled = false v.isEnabled = false
listener.delete(item) listener.delete(item)
} }
} }
} }
} }

View file

@ -26,10 +26,11 @@ import com.google.android.material.tabs.TabLayoutMediator
import com.keylesspalace.tusky.BottomSheetActivity import com.keylesspalace.tusky.BottomSheetActivity
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.search.adapter.SearchPagerAdapter import com.keylesspalace.tusky.components.search.adapter.SearchPagerAdapter
import com.keylesspalace.tusky.databinding.ActivitySearchBinding
import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.util.viewBinding
import dagger.android.DispatchingAndroidInjector import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector import dagger.android.HasAndroidInjector
import kotlinx.android.synthetic.main.activity_search.*
import javax.inject.Inject import javax.inject.Inject
class SearchActivity : BottomSheetActivity(), HasAndroidInjector { class SearchActivity : BottomSheetActivity(), HasAndroidInjector {
@ -41,10 +42,12 @@ class SearchActivity : BottomSheetActivity(), HasAndroidInjector {
private val viewModel: SearchViewModel by viewModels { viewModelFactory } private val viewModel: SearchViewModel by viewModels { viewModelFactory }
private val binding by viewBinding(ActivitySearchBinding::inflate)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_search) setContentView(binding.root)
setSupportActionBar(toolbar) setSupportActionBar(binding.toolbar)
supportActionBar?.apply { supportActionBar?.apply {
setDisplayHomeAsUpEnabled(true) setDisplayHomeAsUpEnabled(true)
setDisplayShowHomeEnabled(true) setDisplayShowHomeEnabled(true)
@ -55,9 +58,9 @@ class SearchActivity : BottomSheetActivity(), HasAndroidInjector {
} }
private fun setupPages() { private fun setupPages() {
pages.adapter = SearchPagerAdapter(this) binding.pages.adapter = SearchPagerAdapter(this)
TabLayoutMediator(tabs, pages) { TabLayoutMediator(binding.tabs, binding.pages) {
tab, position -> tab, position ->
tab.text = getPageTitle(position) tab.text = getPageTitle(position)
}.attach() }.attach()

View file

@ -193,7 +193,7 @@ class SearchViewModel @Inject constructor(
return accountManager.getAllAccountsOrderedByActive() return accountManager.getAllAccountsOrderedByActive()
} }
fun muteAccount(accountId: String, notifications: Boolean, duration: Int) { fun muteAccount(accountId: String, notifications: Boolean, duration: Int?) {
timelineCases.mute(accountId, notifications, duration) timelineCases.mute(accountId, notifications, duration)
} }

View file

@ -19,24 +19,23 @@ import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.paging.PagedListAdapter import androidx.paging.PagedListAdapter
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import com.keylesspalace.tusky.databinding.ItemHashtagBinding
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.adapter.HashtagViewHolder
import com.keylesspalace.tusky.entity.HashTag import com.keylesspalace.tusky.entity.HashTag
import com.keylesspalace.tusky.interfaces.LinkListener import com.keylesspalace.tusky.interfaces.LinkListener
import com.keylesspalace.tusky.util.BindingHolder
class SearchHashtagsAdapter(private val linkListener: LinkListener) class SearchHashtagsAdapter(private val linkListener: LinkListener)
: PagedListAdapter<HashTag, RecyclerView.ViewHolder>(HASHTAG_COMPARATOR) { : PagedListAdapter<HashTag, BindingHolder<ItemHashtagBinding>>(HASHTAG_COMPARATOR) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemHashtagBinding> {
val view = LayoutInflater.from(parent.context) val binding = ItemHashtagBinding.inflate(LayoutInflater.from(parent.context), parent, false)
.inflate(R.layout.item_hashtag, parent, false) return BindingHolder(binding)
return HashtagViewHolder(view)
} }
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onBindViewHolder(holder: BindingHolder<ItemHashtagBinding>, position: Int) {
getItem(position)?.let { (name) -> 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) }
} }
} }

View file

@ -23,11 +23,10 @@ import com.keylesspalace.tusky.components.search.adapter.SearchAccountsAdapter
import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.NetworkState import com.keylesspalace.tusky.util.NetworkState
import kotlinx.android.synthetic.main.fragment_search.*
class SearchAccountsFragment : SearchFragment<Account>() { class SearchAccountsFragment : SearchFragment<Account>() {
override fun createAdapter(): PagedListAdapter<Account, *> { override fun createAdapter(): PagedListAdapter<Account, *> {
val preferences = PreferenceManager.getDefaultSharedPreferences(searchRecyclerView.context) val preferences = PreferenceManager.getDefaultSharedPreferences(binding.searchRecyclerView.context)
return SearchAccountsAdapter( return SearchAccountsAdapter(
this, this,
@ -46,5 +45,4 @@ class SearchAccountsFragment : SearchFragment<Account>() {
companion object { companion object {
fun newInstance() = SearchAccountsFragment() fun newInstance() = SearchAccountsFragment()
} }
} }

View file

@ -17,11 +17,11 @@ import com.keylesspalace.tusky.BottomSheetActivity
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.ViewTagActivity import com.keylesspalace.tusky.ViewTagActivity
import com.keylesspalace.tusky.components.search.SearchViewModel import com.keylesspalace.tusky.components.search.SearchViewModel
import com.keylesspalace.tusky.databinding.FragmentSearchBinding
import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.interfaces.LinkListener import com.keylesspalace.tusky.interfaces.LinkListener
import com.keylesspalace.tusky.util.* import com.keylesspalace.tusky.util.*
import kotlinx.android.synthetic.main.fragment_search.*
import javax.inject.Inject import javax.inject.Inject
abstract class SearchFragment<T> : Fragment(R.layout.fragment_search), abstract class SearchFragment<T> : Fragment(R.layout.fragment_search),
@ -32,6 +32,8 @@ abstract class SearchFragment<T> : Fragment(R.layout.fragment_search),
protected val viewModel: SearchViewModel by activityViewModels { viewModelFactory } protected val viewModel: SearchViewModel by activityViewModels { viewModelFactory }
protected val binding by viewBinding(FragmentSearchBinding::bind)
private var snackbarErrorRetry: Snackbar? = null private var snackbarErrorRetry: Snackbar? = null
abstract fun createAdapter(): PagedListAdapter<T, *> abstract fun createAdapter(): PagedListAdapter<T, *>
@ -48,8 +50,8 @@ abstract class SearchFragment<T> : Fragment(R.layout.fragment_search),
} }
private fun setupSwipeRefreshLayout() { private fun setupSwipeRefreshLayout() {
swipeRefreshLayout.setOnRefreshListener(this) binding.swipeRefreshLayout.setOnRefreshListener(this)
swipeRefreshLayout.setColorSchemeResources(R.color.chinwag_green) binding.swipeRefreshLayout.setColorSchemeResources(R.color.chinwag_green)
} }
private fun subscribeObservables() { private fun subscribeObservables() {
@ -59,7 +61,7 @@ abstract class SearchFragment<T> : Fragment(R.layout.fragment_search),
networkStateRefresh.observe(viewLifecycleOwner) { networkStateRefresh.observe(viewLifecycleOwner) {
searchProgressBar.visible(it == NetworkState.LOADING) binding.searchProgressBar.visible(it == NetworkState.LOADING)
if (it.status == Status.FAILED) { if (it.status == Status.FAILED) {
showError() showError()
@ -69,7 +71,7 @@ abstract class SearchFragment<T> : Fragment(R.layout.fragment_search),
networkState.observe(viewLifecycleOwner) { networkState.observe(viewLifecycleOwner) {
progressBarBottom.visible(it == NetworkState.LOADING) binding.progressBarBottom.visible(it == NetworkState.LOADING)
if (it.status == Status.FAILED) { if (it.status == Status.FAILED) {
showError() showError()
@ -82,24 +84,25 @@ abstract class SearchFragment<T> : Fragment(R.layout.fragment_search),
} }
private fun initAdapter() { private fun initAdapter() {
searchRecyclerView.addItemDecoration(DividerItemDecoration(searchRecyclerView.context, DividerItemDecoration.VERTICAL)) binding.searchRecyclerView.addItemDecoration(DividerItemDecoration(binding.searchRecyclerView.context, DividerItemDecoration.VERTICAL))
searchRecyclerView.layoutManager = LinearLayoutManager(searchRecyclerView.context) binding.searchRecyclerView.layoutManager = LinearLayoutManager(binding.searchRecyclerView.context)
adapter = createAdapter() adapter = createAdapter()
searchRecyclerView.adapter = adapter binding.searchRecyclerView.adapter = adapter
searchRecyclerView.setHasFixedSize(true) binding.searchRecyclerView.setHasFixedSize(true)
(searchRecyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false (binding.searchRecyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
} }
private fun showNoData(isEmpty: Boolean) { private fun showNoData(isEmpty: Boolean) {
if (isEmpty && networkStateRefresh.value == NetworkState.LOADED) if (isEmpty && networkStateRefresh.value == NetworkState.LOADED) {
searchNoResultsText.show() binding.searchNoResultsText.show()
else } else {
searchNoResultsText.hide() binding.searchNoResultsText.hide()
}
} }
private fun showError() { private fun showError() {
if (snackbarErrorRetry?.isShown != true) { 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?.setAction(R.string.action_retry) {
snackbarErrorRetry = null snackbarErrorRetry = null
viewModel.retryAllSearches() viewModel.retryAllSearches()
@ -122,8 +125,8 @@ abstract class SearchFragment<T> : Fragment(R.layout.fragment_search),
override fun onRefresh() { override fun onRefresh() {
// Dismissed here because the RecyclerView bottomProgressBar is shown as soon as the retry begins. // Dismissed here because the RecyclerView bottomProgressBar is shown as soon as the retry begins.
swipeRefreshLayout.post { binding.swipeRefreshLayout.post {
swipeRefreshLayout.isRefreshing = false binding.swipeRefreshLayout.isRefreshing = false
} }
viewModel.retryAllSearches() viewModel.retryAllSearches()
} }

View file

@ -63,7 +63,6 @@ import com.keylesspalace.tusky.viewdata.StatusViewData
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from
import com.uber.autodispose.autoDispose import com.uber.autodispose.autoDispose
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.synthetic.main.fragment_search.*
class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concrete>>(), StatusActionListener { class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concrete>>(), StatusActionListener {
@ -78,7 +77,7 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
get() = super.adapter as SearchStatusesAdapter get() = super.adapter as SearchStatusesAdapter
override fun createAdapter(): PagedListAdapter<Pair<Status, StatusViewData.Concrete>, *> { override fun createAdapter(): PagedListAdapter<Pair<Status, StatusViewData.Concrete>, *> {
val preferences = PreferenceManager.getDefaultSharedPreferences(searchRecyclerView.context) val preferences = PreferenceManager.getDefaultSharedPreferences(binding.searchRecyclerView.context)
val statusDisplayOptions = StatusDisplayOptions( val statusDisplayOptions = StatusDisplayOptions(
animateAvatars = preferences.getBoolean("animateGifAvatars", false), animateAvatars = preferences.getBoolean("animateGifAvatars", false),
mediaPreviewEnabled = viewModel.mediaPreviewEnabled, mediaPreviewEnabled = viewModel.mediaPreviewEnabled,
@ -91,12 +90,11 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
) )
searchRecyclerView.addItemDecoration(DividerItemDecoration(searchRecyclerView.context, DividerItemDecoration.VERTICAL)) binding.searchRecyclerView.addItemDecoration(DividerItemDecoration(binding.searchRecyclerView.context, DividerItemDecoration.VERTICAL))
searchRecyclerView.layoutManager = LinearLayoutManager(searchRecyclerView.context) binding.searchRecyclerView.layoutManager = LinearLayoutManager(binding.searchRecyclerView.context)
return SearchStatusesAdapter(statusDisplayOptions, this) return SearchStatusesAdapter(statusDisplayOptions, this)
} }
override fun onContentHiddenChange(isShowing: Boolean, position: Int) { override fun onContentHiddenChange(isShowing: Boolean, position: Int) {
searchAdapter.getItem(position)?.let { searchAdapter.getItem(position)?.let {
viewModel.contentHiddenChange(it, isShowing) viewModel.contentHiddenChange(it, isShowing)
@ -486,5 +484,4 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
.show() .show()
} }
} }
} }

View file

@ -18,24 +18,26 @@ package com.keylesspalace.tusky.db
import android.text.Spanned import android.text.Spanned
import androidx.core.text.parseAsHtml import androidx.core.text.parseAsHtml
import androidx.core.text.toHtml import androidx.core.text.toHtml
import androidx.room.ProvidedTypeConverter
import androidx.room.TypeConverter import androidx.room.TypeConverter
import com.google.gson.GsonBuilder import com.google.gson.Gson
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import com.keylesspalace.tusky.TabData import com.keylesspalace.tusky.TabData
import com.keylesspalace.tusky.components.conversation.ConversationAccountEntity import com.keylesspalace.tusky.components.conversation.ConversationAccountEntity
import com.keylesspalace.tusky.createTabDataFromId import com.keylesspalace.tusky.createTabDataFromId
import com.keylesspalace.tusky.entity.* import com.keylesspalace.tusky.entity.*
import com.keylesspalace.tusky.json.SpannedTypeAdapter
import com.keylesspalace.tusky.util.trimTrailingWhitespace import com.keylesspalace.tusky.util.trimTrailingWhitespace
import java.net.URLDecoder import java.net.URLDecoder
import java.net.URLEncoder import java.net.URLEncoder
import java.util.* import java.util.*
import javax.inject.Inject
import javax.inject.Singleton
class Converters { @ProvidedTypeConverter
@Singleton
private val gson = GsonBuilder() class Converters @Inject constructor (
.registerTypeAdapter(Spanned::class.java, SpannedTypeAdapter()) private val gson: Gson
.create() ) {
@TypeConverter @TypeConverter
fun jsonToEmojiList(emojiListJson: String?): List<Emoji>? { fun jsonToEmojiList(emojiListJson: String?): List<Emoji>? {

View file

@ -23,7 +23,7 @@ import androidx.room.PrimaryKey
import androidx.room.TypeConverters import androidx.room.TypeConverters
import com.keylesspalace.tusky.entity.NewPoll import com.keylesspalace.tusky.entity.NewPoll
import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.entity.Status
import kotlinx.android.parcel.Parcelize import kotlinx.parcelize.Parcelize
@Entity @Entity
@TypeConverters(Converters::class) @TypeConverters(Converters::class)

View file

@ -28,6 +28,7 @@ import com.keylesspalace.tusky.appstore.EventHubImpl
import com.keylesspalace.tusky.components.notifications.Notifier import com.keylesspalace.tusky.components.notifications.Notifier
import com.keylesspalace.tusky.components.notifications.SystemNotifier import com.keylesspalace.tusky.components.notifications.SystemNotifier
import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.db.Converters
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.network.TimelineCases import com.keylesspalace.tusky.network.TimelineCases
import com.keylesspalace.tusky.network.TimelineCasesImpl import com.keylesspalace.tusky.network.TimelineCasesImpl
@ -70,8 +71,9 @@ class AppModule {
@Provides @Provides
@Singleton @Singleton
fun providesDatabase(appContext: Context): AppDatabase { fun providesDatabase(appContext: Context, converters: Converters): AppDatabase {
return Room.databaseBuilder(appContext, AppDatabase::class.java, "tuskyDB") return Room.databaseBuilder(appContext, AppDatabase::class.java, "tuskyDB")
.addTypeConverter(converters)
.allowMainThreadQueries() .allowMainThreadQueries()
.addMigrations(AppDatabase.MIGRATION_2_3, AppDatabase.MIGRATION_3_4, AppDatabase.MIGRATION_4_5, .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, AppDatabase.MIGRATION_5_6, AppDatabase.MIGRATION_6_7, AppDatabase.MIGRATION_7_8,

View file

@ -22,7 +22,7 @@ import com.google.gson.JsonElement
import com.google.gson.JsonParseException import com.google.gson.JsonParseException
import com.google.gson.annotations.JsonAdapter import com.google.gson.annotations.JsonAdapter
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
import kotlinx.android.parcel.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class Attachment( data class Attachment(

View file

@ -17,7 +17,7 @@ package com.keylesspalace.tusky.entity
import android.os.Parcelable import android.os.Parcelable
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
import kotlinx.android.parcel.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class Emoji( data class Emoji(

View file

@ -17,7 +17,7 @@ package com.keylesspalace.tusky.entity
import android.os.Parcelable import android.os.Parcelable
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
import kotlinx.android.parcel.Parcelize import kotlinx.parcelize.Parcelize
data class NewStatus( data class NewStatus(
val status: String, val status: String,

View file

@ -21,6 +21,7 @@ import android.view.View
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -31,6 +32,8 @@ import com.keylesspalace.tusky.AccountListActivity.Type
import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.BaseActivity
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.adapter.* 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.di.Injectable
import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.entity.Relationship 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.HttpHeaderLink
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.view.EndlessOnScrollListener import com.keylesspalace.tusky.view.EndlessOnScrollListener
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from
import com.uber.autodispose.autoDispose import com.uber.autodispose.autoDispose
import io.reactivex.Single import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.synthetic.main.fragment_account_list.*
import retrofit2.Response import retrofit2.Response
import java.io.IOException import java.io.IOException
import java.util.* import java.util.*
@ -55,6 +58,10 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct
@Inject @Inject
lateinit var api: MastodonApi lateinit var api: MastodonApi
@Inject
lateinit var accountManager: AccountManager
private val binding by viewBinding(FragmentAccountListBinding::bind)
private lateinit var type: Type private lateinit var type: Type
private var id: String? = null 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?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
recyclerView.setHasFixedSize(true) binding.recyclerView.setHasFixedSize(true)
val layoutManager = LinearLayoutManager(view.context) val layoutManager = LinearLayoutManager(view.context)
recyclerView.layoutManager = layoutManager binding.recyclerView.layoutManager = layoutManager
(recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false (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 pm = PreferenceManager.getDefaultSharedPreferences(view.context)
val animateAvatar = pm.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false) 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) { adapter = when (type) {
Type.BLOCKS -> BlocksAdapter(this, animateAvatar, animateEmojis) Type.BLOCKS -> BlocksAdapter(this, animateAvatar, animateEmojis)
Type.MUTES -> MutesAdapter(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) else -> FollowAdapter(this, animateAvatar, animateEmojis)
} }
recyclerView.adapter = adapter if (binding.recyclerView.adapter == null) {
binding.recyclerView.adapter = adapter
}
scrollListener = object : EndlessOnScrollListener(layoutManager) { scrollListener = object : EndlessOnScrollListener(layoutManager) {
override fun onLoadMore(totalItemsCount: Int, view: RecyclerView) { 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() fetchAccounts()
} }
@ -136,7 +150,7 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct
val unmutedUser = mutesAdapter.removeItem(position) val unmutedUser = mutesAdapter.removeItem(position)
if (unmutedUser != null) { 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) { .setAction(R.string.action_undo) {
mutesAdapter.addItem(unmutedUser, position) mutesAdapter.addItem(unmutedUser, position)
onMute(true, id, position, notifications) onMute(true, id, position, notifications)
@ -180,7 +194,7 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct
val unblockedUser = blocksAdapter.removeItem(position) val unblockedUser = blocksAdapter.removeItem(position)
if (unblockedUser != null) { 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) { .setAction(R.string.action_undo) {
blocksAdapter.addItem(unblockedUser, position) blocksAdapter.addItem(unblockedUser, position)
onBlock(true, id, position) onBlock(true, id, position)
@ -260,7 +274,7 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct
fetching = true fetching = true
if (fromId != null) { if (fromId != null) {
recyclerView.post { adapter.setBottomLoading(true) } binding.recyclerView.post { adapter.setBottomLoading(true) }
} }
getFetchCallByListType(fromId) getFetchCallByListType(fromId)
@ -303,14 +317,14 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct
fetching = false fetching = false
if (adapter.itemCount == 0) { if (adapter.itemCount == 0) {
messageView.show() binding.messageView.show()
messageView.setup( binding.messageView.setup(
R.drawable.elephant_friend_empty, R.drawable.elephant_friend_empty,
R.string.message_empty, R.string.message_empty,
null null
) )
} else { } 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) Log.e(TAG, "Fetch failure", throwable)
if (adapter.itemCount == 0) { if (adapter.itemCount == 0) {
messageView.show() binding.messageView.show()
if (throwable is IOException) { if (throwable is IOException) {
messageView.setup(R.drawable.elephant_offline, R.string.error_network) { binding.messageView.setup(R.drawable.elephant_offline, R.string.error_network) {
messageView.hide() binding.messageView.hide()
this.fetchAccounts(null) this.fetchAccounts(null)
} }
} else { } else {
messageView.setup(R.drawable.elephant_error, R.string.error_generic) { binding.messageView.setup(R.drawable.elephant_error, R.string.error_generic) {
messageView.hide() binding.messageView.hide()
this.fetchAccounts(null) 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 TAG = "AccountList" // logging tag
private const val ARG_TYPE = "type" private const val ARG_TYPE = "type"
private const val ARG_ID = "id" 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 { return AccountListFragment().apply {
arguments = Bundle(2).apply { arguments = Bundle(2).apply {
putSerializable(ARG_TYPE, type) putSerializable(ARG_TYPE, type)
putString(ARG_ID, id) putString(ARG_ID, id)
putBoolean(ARG_ACCOUNT_LOCKED, accountLocked)
} }
} }
} }
} }
} }

View file

@ -30,6 +30,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.ViewMediaActivity import com.keylesspalace.tusky.ViewMediaActivity
import com.keylesspalace.tusky.databinding.FragmentTimelineBinding
import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.Status 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.ThemeUtils
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.view.SquareImageView import com.keylesspalace.tusky.view.SquareImageView
import com.keylesspalace.tusky.viewdata.AttachmentViewData import com.keylesspalace.tusky.viewdata.AttachmentViewData
import com.uber.autodispose.android.lifecycle.autoDispose import com.uber.autodispose.android.lifecycle.autoDispose
import io.reactivex.SingleObserver import io.reactivex.SingleObserver
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposable
import kotlinx.android.synthetic.main.fragment_timeline.*
import retrofit2.Response import retrofit2.Response
import java.io.IOException import java.io.IOException
import java.util.* import java.util.*
@ -58,49 +59,36 @@ import javax.inject.Inject
*/ */
class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFragment, Injectable { 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 @Inject
lateinit var api: MastodonApi lateinit var api: MastodonApi
private val binding by viewBinding(FragmentTimelineBinding::bind)
private lateinit var accountId: String
private val adapter = MediaGridAdapter() private val adapter = MediaGridAdapter()
private val statuses = mutableListOf<Status>() private val statuses = mutableListOf<Status>()
private var fetchingStatus = FetchingStatus.NOT_FETCHING 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<Response<List<Status>>> { private val callback = object : SingleObserver<Response<List<Status>>> {
override fun onError(t: Throwable) { override fun onError(t: Throwable) {
fetchingStatus = FetchingStatus.NOT_FETCHING fetchingStatus = FetchingStatus.NOT_FETCHING
if (isAdded) { if (isAdded) {
swipeRefreshLayout.isRefreshing = false binding.swipeRefreshLayout.isRefreshing = false
progressBar.visibility = View.GONE binding.progressBar.visibility = View.GONE
topProgressBar?.hide() binding.topProgressBar.hide()
statusView.show() binding.statusView.show()
if (t is IOException) { 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() doInitialLoadingIfNeeded()
} }
} else { } else {
statusView.setup(R.drawable.elephant_error, R.string.error_generic) { binding.statusView.setup(R.drawable.elephant_error, R.string.error_generic) {
doInitialLoadingIfNeeded() doInitialLoadingIfNeeded()
} }
} }
@ -112,9 +100,9 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr
override fun onSuccess(response: Response<List<Status>>) { override fun onSuccess(response: Response<List<Status>>) {
fetchingStatus = FetchingStatus.NOT_FETCHING fetchingStatus = FetchingStatus.NOT_FETCHING
if (isAdded) { if (isAdded) {
swipeRefreshLayout.isRefreshing = false binding.swipeRefreshLayout.isRefreshing = false
progressBar.visibility = View.GONE binding.progressBar.visibility = View.GONE
topProgressBar?.hide() binding.topProgressBar.hide()
val body = response.body() val body = response.body()
body?.let { fetched -> body?.let { fetched ->
@ -126,11 +114,11 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr
} }
adapter.addTop(result) adapter.addTop(result)
if (result.isNotEmpty()) if (result.isNotEmpty())
recyclerView.scrollToPosition(0) binding.recyclerView.scrollToPosition(0)
if (statuses.isEmpty()) { if (statuses.isEmpty()) {
statusView.show() binding.statusView.show()
statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty) 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) adapter.baseItemColor = ThemeUtils.getColor(view.context, android.R.attr.windowBackground)
recyclerView.layoutManager = layoutManager binding.recyclerView.layoutManager = layoutManager
recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
if (isSwipeToRefreshEnabled) { if (isSwipeToRefreshEnabled) {
swipeRefreshLayout.setOnRefreshListener { binding.swipeRefreshLayout.setOnRefreshListener {
refresh() 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) { override fun onScrolled(recycler_view: RecyclerView, dx: Int, dy: Int) {
if (dy > 0) { if (dy > 0) {
@ -216,7 +204,7 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr
} }
private fun refresh() { private fun refresh() {
statusView.hide() binding.statusView.hide()
if (fetchingStatus != FetchingStatus.NOT_FETCHING) return if (fetchingStatus != FetchingStatus.NOT_FETCHING) return
if (statuses.isEmpty()) { if (statuses.isEmpty()) {
fetchingStatus = FetchingStatus.INITIAL_FETCHING fetchingStatus = FetchingStatus.INITIAL_FETCHING
@ -229,12 +217,12 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr
.subscribe(callback) .subscribe(callback)
if (!isSwipeToRefreshEnabled) if (!isSwipeToRefreshEnabled)
topProgressBar?.show() binding.topProgressBar.show()
} }
private fun doInitialLoadingIfNeeded() { private fun doInitialLoadingIfNeeded() {
if (isAdded) { if (isAdded) {
statusView.hide() binding.statusView.hide()
} }
if (fetchingStatus == FetchingStatus.NOT_FETCHING && statuses.isEmpty()) { if (fetchingStatus == FetchingStatus.NOT_FETCHING && statuses.isEmpty()) {
fetchingStatus = FetchingStatus.INITIAL_FETCHING fetchingStatus = FetchingStatus.INITIAL_FETCHING
@ -332,7 +320,7 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr
// saving some allocations // saving some allocations
override fun onClick(v: View?) { 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 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"
}
} }

View file

@ -863,6 +863,7 @@ public class NotificationsFragment extends SFragment implements
adapter.setMediaPreviewEnabled(enabled); adapter.setMediaPreviewEnabled(enabled);
fullyRefresh(); fullyRefresh();
} }
break;
} }
case "showNotificationsFilter": { case "showNotificationsFilter": {
if (isAdded()) { if (isAdded()) {
@ -870,6 +871,7 @@ public class NotificationsFragment extends SFragment implements
updateFilterVisibility(); updateFilterVisibility();
fullyRefreshWithProgressBar(true); fullyRefreshWithProgressBar(true);
} }
break;
} }
} }
} }

View file

@ -511,7 +511,7 @@ public abstract class SFragment extends Fragment implements Injectable {
}); });
} }
@VisibleForTesting @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
public void reloadFilters(boolean forceRefresh) { public void reloadFilters(boolean forceRefresh) {
if (filters != null && !forceRefresh) { if (filters != null && !forceRefresh) {
applyFilters(forceRefresh); applyFilters(forceRefresh);
@ -547,7 +547,7 @@ public abstract class SFragment extends Fragment implements Injectable {
// Override to refresh your fragment // Override to refresh your fragment
} }
@VisibleForTesting @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
public boolean shouldFilterStatus(Status status) { public boolean shouldFilterStatus(Status status) {
if (filterRemoveRegex && status.getPoll() != null) { if (filterRemoveRegex && status.getPoll() != null) {

View file

@ -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 <http://www.gnu.org/licenses>. */
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);
}
}

File diff suppressed because it is too large Load diff

View file

@ -32,13 +32,12 @@ import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target import com.bumptech.glide.request.target.Target
import com.github.chrisbanes.photoview.PhotoViewAttacher 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.entity.Attachment
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.visible import com.keylesspalace.tusky.util.visible
import io.reactivex.subjects.BehaviorSubject 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 import kotlin.math.abs
class ViewImageFragment : ViewMediaFragment() { class ViewImageFragment : ViewMediaFragment() {
@ -48,6 +47,9 @@ class ViewImageFragment : ViewMediaFragment() {
fun onPhotoTap() fun onPhotoTap()
} }
private var _binding: FragmentViewImageBinding? = null
private val binding get() = _binding!!
private lateinit var attacher: PhotoViewAttacher private lateinit var attacher: PhotoViewAttacher
private lateinit var photoActionsListener: PhotoActionsListener private lateinit var photoActionsListener: PhotoActionsListener
private lateinit var toolbar: View private lateinit var toolbar: View
@ -71,18 +73,19 @@ class ViewImageFragment : ViewMediaFragment() {
description: String?, description: String?,
showingDescription: Boolean showingDescription: Boolean
) { ) {
photoView.transitionName = url binding.photoView.transitionName = url
mediaDescription.text = description binding.mediaDescription.text = description
captionSheet.visible(showingDescription) binding.captionSheet.visible(showingDescription)
startedTransition = false startedTransition = false
loadImageFromNetwork(url, previewUrl, photoView) loadImageFromNetwork(url, previewUrl, binding.photoView)
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
toolbar = requireActivity().toolbar toolbar = (requireActivity() as ViewMediaActivity).toolbar
this.transition = BehaviorSubject.create() 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") @SuppressLint("ClickableViewAccessibility")
@ -105,7 +108,7 @@ class ViewImageFragment : ViewMediaFragment() {
} }
} }
attacher = PhotoViewAttacher(photoView).apply { attacher = PhotoViewAttacher(binding.photoView).apply {
// This prevents conflicts with ViewPager // This prevents conflicts with ViewPager
setAllowParentInterceptOnEdge(true) setAllowParentInterceptOnEdge(true)
@ -127,7 +130,7 @@ class ViewImageFragment : ViewMediaFragment() {
var lastY = 0f var lastY = 0f
photoView.setOnTouchListener { v, event -> binding.photoView.setOnTouchListener { v, event ->
// This part is for scaling/translating on vertical move. // This part is for scaling/translating on vertical move.
// We use raw coordinates to get the correct ones during scaling // We use raw coordinates to get the correct ones during scaling
@ -140,11 +143,11 @@ class ViewImageFragment : ViewMediaFragment() {
val diff = event.rawY - lastY val diff = event.rawY - lastY
// This code is to prevent transformations during page scrolling // This code is to prevent transformations during page scrolling
// If we are already translating or we reached the threshold, then transform. // If we are already translating or we reached the threshold, then transform.
if (photoView.translationY != 0f || abs(diff) > 40) { if (binding.photoView.translationY != 0f || abs(diff) > 40) {
photoView.translationY += (diff) binding.photoView.translationY += (diff)
val scale = (-abs(photoView.translationY) / 720 + 1).coerceAtLeast(0.5f) val scale = (-abs(binding.photoView.translationY) / 720 + 1).coerceAtLeast(0.5f)
photoView.scaleY = scale binding.photoView.scaleY = scale
photoView.scaleX = scale binding.photoView.scaleX = scale
lastY = event.rawY lastY = event.rawY
return@setOnTouchListener true return@setOnTouchListener true
} }
@ -158,13 +161,13 @@ class ViewImageFragment : ViewMediaFragment() {
} }
private fun onGestureEnd() { private fun onGestureEnd() {
if (photoView == null) { if (_binding == null) {
return return
} }
if (abs(photoView.translationY) > 180) { if (abs(binding.photoView.translationY) > 180) {
photoActionsListener.onDismiss() photoActionsListener.onDismiss()
} else { } 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) { override fun onToolbarVisibilityChange(visible: Boolean) {
if (photoView == null || !userVisibleHint || captionSheet == null) { if (_binding == null || !userVisibleHint ) {
return return
} }
isDescriptionVisible = showingDescription && visible isDescriptionVisible = showingDescription && visible
val alpha = if (isDescriptionVisible) 1.0f else 0.0f val alpha = if (isDescriptionVisible) 1.0f else 0.0f
captionSheet.animate().alpha(alpha) binding.captionSheet.animate().alpha(alpha)
.setListener(object : AnimatorListenerAdapter() { .setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) { override fun onAnimationEnd(animation: Animator) {
captionSheet?.visible(isDescriptionVisible) if (_binding != null) {
binding.captionSheet.visible(isDescriptionVisible)
}
animation.removeListener(this) animation.removeListener(this)
} }
}) })
@ -189,8 +194,9 @@ class ViewImageFragment : ViewMediaFragment() {
} }
override fun onDestroyView() { override fun onDestroyView() {
Glide.with(this).clear(photoView) Glide.with(this).clear(binding.photoView)
transition.onComplete() transition.onComplete()
_binding = null
super.onDestroyView() super.onDestroyView()
} }
@ -253,7 +259,7 @@ class ViewImageFragment : ViewMediaFragment() {
photoActionsListener.onBringUp() photoActionsListener.onBringUp()
} }
// Hide progress bar only on fail request from internet // 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 // We don't want to overwrite preview with null when main image fails to load
return !isCacheRequest return !isCacheRequest
} }
@ -261,14 +267,16 @@ class ViewImageFragment : ViewMediaFragment() {
@SuppressLint("CheckResult") @SuppressLint("CheckResult")
override fun onResourceReady(resource: Drawable, model: Any, target: Target<Drawable>, override fun onResourceReady(resource: Drawable, model: Any, target: Target<Drawable>,
dataSource: DataSource, isFirstResource: Boolean): Boolean { 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) { if (!startedTransition || !shouldStartTransition) {
// Set this right away so that we don't have to concurrent post() requests // Set this right away so that we don't have to concurrent post() requests
startedTransition = true startedTransition = true
// post() because load() replaces image with null. Sometimes after we set // post() because load() replaces image with null. Sometimes after we set
// the thumbnail. // the thumbnail.
photoView.post { binding.photoView.post {
target.onResourceReady(resource, null) target.onResourceReady(resource, null)
if (shouldStartTransition) photoActionsListener.onBringUp() if (shouldStartTransition) photoActionsListener.onBringUp()
} }

View file

@ -26,16 +26,18 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.MediaController import android.widget.MediaController
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.ViewMediaActivity import com.keylesspalace.tusky.ViewMediaActivity
import com.keylesspalace.tusky.databinding.FragmentViewVideoBinding
import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.visible import com.keylesspalace.tusky.util.visible
import com.keylesspalace.tusky.view.ExposedPlayPauseVideoView 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() { class ViewVideoFragment : ViewMediaFragment() {
private var _binding: FragmentViewVideoBinding? = null
private val binding get() = _binding!!
private lateinit var toolbar: View private lateinit var toolbar: View
private val handler = Handler(Looper.getMainLooper()) private val handler = Handler(Looper.getMainLooper())
private val hideToolbar = Runnable { private val hideToolbar = Runnable {
@ -52,7 +54,7 @@ class ViewVideoFragment : ViewMediaFragment() {
override fun setUserVisibleHint(isVisibleToUser: Boolean) { override fun setUserVisibleHint(isVisibleToUser: Boolean) {
// Start/pause/resume video playback as fragment is shown/hidden // Start/pause/resume video playback as fragment is shown/hidden
super.setUserVisibleHint(isVisibleToUser) super.setUserVisibleHint(isVisibleToUser)
if (videoView == null) { if (_binding == null) {
return return
} }
@ -60,10 +62,10 @@ class ViewVideoFragment : ViewMediaFragment() {
if (mediaActivity.isToolbarVisible) { if (mediaActivity.isToolbarVisible) {
handler.postDelayed(hideToolbar, TOOLBAR_HIDE_DELAY_MS) handler.postDelayed(hideToolbar, TOOLBAR_HIDE_DELAY_MS)
} }
videoView.start() binding.videoView.start()
} else { } else {
handler.removeCallbacks(hideToolbar) handler.removeCallbacks(hideToolbar)
videoView.pause() binding.videoView.pause()
mediaController.hide() mediaController.hide()
} }
} }
@ -75,11 +77,11 @@ class ViewVideoFragment : ViewMediaFragment() {
description: String?, description: String?,
showingDescription: Boolean showingDescription: Boolean
) { ) {
mediaDescription.text = description binding.mediaDescription.text = description
mediaDescription.visible(showingDescription) binding.mediaDescription.visible(showingDescription)
videoView.transitionName = url binding.videoView.transitionName = url
videoView.setVideoPath(url) binding.videoView.setVideoPath(url)
mediaController = object : MediaController(mediaActivity) { mediaController = object : MediaController(mediaActivity) {
override fun show(timeout: Int) { override fun show(timeout: Int) {
// We're doing manual auto-close management. // We're doing manual auto-close management.
@ -100,10 +102,10 @@ class ViewVideoFragment : ViewMediaFragment() {
} }
} }
mediaController.setMediaPlayer(videoView) mediaController.setMediaPlayer(binding.videoView)
videoView.setMediaController(mediaController) binding.videoView.setMediaController(mediaController)
videoView.requestFocus() binding.videoView.requestFocus()
videoView.setPlayPauseListener(object: ExposedPlayPauseVideoView.PlayPauseListener { binding.videoView.setPlayPauseListener(object: ExposedPlayPauseVideoView.PlayPauseListener {
override fun onPause() { override fun onPause() {
handler.removeCallbacks(hideToolbar) handler.removeCallbacks(hideToolbar)
} }
@ -117,31 +119,31 @@ class ViewVideoFragment : ViewMediaFragment() {
} }
} }
}) })
videoView.setOnPreparedListener { mp -> binding.videoView.setOnPreparedListener { mp ->
val containerWidth = videoContainer.measuredWidth.toFloat() val containerWidth = binding.videoContainer.measuredWidth.toFloat()
val containerHeight = videoContainer.measuredHeight.toFloat() val containerHeight = binding.videoContainer.measuredHeight.toFloat()
val videoWidth = mp.videoWidth.toFloat() val videoWidth = mp.videoWidth.toFloat()
val videoHeight = mp.videoHeight.toFloat() val videoHeight = mp.videoHeight.toFloat()
if(containerWidth/containerHeight > videoWidth/videoHeight) { if(containerWidth/containerHeight > videoWidth/videoHeight) {
videoView.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT binding.videoView.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
videoView.layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT binding.videoView.layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT
} else { } else {
videoView.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT binding.videoView.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT
videoView.layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT binding.videoView.layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT
} }
// Wait until the media is loaded before accepting taps as we don't want toolbar to // Wait until the media is loaded before accepting taps as we don't want toolbar to
// be hidden until then. // be hidden until then.
videoView.setOnTouchListener { _, _ -> binding.videoView.setOnTouchListener { _, _ ->
mediaActivity.onPhotoTap() mediaActivity.onPhotoTap()
false false
} }
progressBar.hide() binding.progressBar.hide()
mp.isLooping = true mp.isLooping = true
if (requireArguments().getBoolean(ARG_START_POSTPONED_TRANSITION)) { 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 { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
toolbar = requireActivity().toolbar
mediaActivity = activity as ViewMediaActivity 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?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -174,7 +177,7 @@ class ViewVideoFragment : ViewMediaFragment() {
} }
override fun onToolbarVisibilityChange(visible: Boolean) { override fun onToolbarVisibilityChange(visible: Boolean) {
if (videoView == null || mediaDescription == null || !userVisibleHint) { if (_binding == null || !userVisibleHint) {
return return
} }
@ -182,20 +185,22 @@ class ViewVideoFragment : ViewMediaFragment() {
val alpha = if (isDescriptionVisible) 1.0f else 0.0f val alpha = if (isDescriptionVisible) 1.0f else 0.0f
if (isDescriptionVisible) { if (isDescriptionVisible) {
// If to be visible, need to make visible immediately and animate alpha // If to be visible, need to make visible immediately and animate alpha
mediaDescription.alpha = 0.0f binding.mediaDescription.alpha = 0.0f
mediaDescription.visible(isDescriptionVisible) binding.mediaDescription.visible(isDescriptionVisible)
} }
mediaDescription.animate().alpha(alpha) binding.mediaDescription.animate().alpha(alpha)
.setListener(object : AnimatorListenerAdapter() { .setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) { override fun onAnimationEnd(animation: Animator) {
mediaDescription?.visible(isDescriptionVisible) if (_binding != null) {
binding.mediaDescription.visible(isDescriptionVisible)
}
animation.removeListener(this) animation.removeListener(this)
} }
}) })
.start() .start()
if (visible && videoView.isPlaying && !isAudio) { if (visible && binding.videoView.isPlaying && !isAudio) {
hideToolbarAfterDelay(TOOLBAR_HIDE_DELAY_MS) hideToolbarAfterDelay(TOOLBAR_HIDE_DELAY_MS)
} else { } else {
handler.removeCallbacks(hideToolbar) handler.removeCallbacks(hideToolbar)
@ -204,4 +209,9 @@ class ViewVideoFragment : ViewMediaFragment() {
override fun onTransitionEnd() { override fun onTransitionEnd() {
} }
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
} }

View file

@ -33,7 +33,7 @@ interface TimelineCases {
fun reblog(status: Status, reblog: Boolean): Single<Status> fun reblog(status: Status, reblog: Boolean): Single<Status>
fun favourite(status: Status, favourite: Boolean): Single<Status> fun favourite(status: Status, favourite: Boolean): Single<Status>
fun bookmark(status: Status, bookmark: Boolean): Single<Status> fun bookmark(status: Status, bookmark: Boolean): Single<Status>
fun mute(id: String, notifications: Boolean, duration: Int) fun mute(id: String, notifications: Boolean, duration: Int?)
fun block(id: String) fun block(id: String)
fun delete(id: String): Single<DeletedStatus> fun delete(id: String): Single<DeletedStatus>
fun pin(status: Status, pin: Boolean) 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) mastodonApi.muteAccount(id, notifications, duration)
.subscribe({ .subscribe({
eventHub.dispatch(MuteEvent(id)) eventHub.dispatch(MuteEvent(id))

View file

@ -298,7 +298,7 @@ fun Account.toEntity(accountId: Long, gson: Gson): TimelineAccountEntity {
timelineUserId = accountId, timelineUserId = accountId,
localUsername = localUsername, localUsername = localUsername,
username = username, username = username,
displayName = displayName.orEmpty(), displayName = name,
url = url, url = url,
avatar = avatar, avatar = avatar,
emojis = gson.toJson(emojis), emojis = gson.toJson(emojis),

View file

@ -28,7 +28,7 @@ import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.SaveTootHelper import com.keylesspalace.tusky.util.SaveTootHelper
import dagger.android.AndroidInjection import dagger.android.AndroidInjection
import kotlinx.android.parcel.Parcelize import kotlinx.parcelize.Parcelize
import retrofit2.Call import retrofit2.Call
import retrofit2.Callback import retrofit2.Callback
import retrofit2.Response import retrofit2.Response

View file

@ -3,6 +3,6 @@ package com.keylesspalace.tusky.util
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
class BindingViewHolder<T : ViewBinding>( class BindingHolder<T : ViewBinding>(
val binding: T val binding: T
) : RecyclerView.ViewHolder(binding.root) ) : RecyclerView.ViewHolder(binding.root)

View file

@ -256,27 +256,27 @@ class EmojiCompatFont(
private const val CHUNK_SIZE = 4096L private const val CHUNK_SIZE = 4096L
// The system font gets some special behavior... // The system font gets some special behavior...
private val SYSTEM_DEFAULT = EmojiCompatFont("system-default", val SYSTEM_DEFAULT = EmojiCompatFont("system-default",
"System Default", "System Default",
R.string.caption_systememoji, R.string.caption_systememoji,
R.drawable.ic_emoji_34dp, R.drawable.ic_emoji_34dp,
"", "",
"0") "0")
private val BLOBMOJI = EmojiCompatFont("Blobmoji", val BLOBMOJI = EmojiCompatFont("Blobmoji",
"Blobmoji", "Blobmoji",
R.string.caption_blobmoji, R.string.caption_blobmoji,
R.drawable.ic_blobmoji, R.drawable.ic_blobmoji,
"https://tusky.app/hosted/emoji/BlobmojiCompat.ttf", "https://tusky.app/hosted/emoji/BlobmojiCompat.ttf",
"12.0.0" "12.0.0"
) )
private val TWEMOJI = EmojiCompatFont("Twemoji", val TWEMOJI = EmojiCompatFont("Twemoji",
"Twemoji", "Twemoji",
R.string.caption_twemoji, R.string.caption_twemoji,
R.drawable.ic_twemoji, R.drawable.ic_twemoji,
"https://tusky.app/hosted/emoji/TwemojiCompat.ttf", "https://tusky.app/hosted/emoji/TwemojiCompat.ttf",
"12.0.0" "12.0.0"
) )
private val NOTOEMOJI = EmojiCompatFont("NotoEmoji", val NOTOEMOJI = EmojiCompatFont("NotoEmoji",
"Noto Emoji", "Noto Emoji",
R.string.caption_notoemoji, R.string.caption_notoemoji,
R.drawable.ic_notoemoji, R.drawable.ic_notoemoji,

View file

@ -22,7 +22,7 @@ import com.keylesspalace.tusky.viewdata.StatusViewData
import kotlin.math.min import kotlin.math.min
// Not using lambdas because there's boxing of int then // Not using lambdas because there's boxing of int then
interface StatusProvider { fun interface StatusProvider {
fun getStatus(pos: Int): StatusViewData? fun getStatus(pos: Int): StatusViewData?
} }

View file

@ -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 <T : ViewBinding> AppCompatActivity.viewBinding(
crossinline bindingInflater: (LayoutInflater) -> T
) = lazy(LazyThreadSafetyMode.NONE) {
bindingInflater(layoutInflater)
}
class FragmentViewBindingDelegate<T : ViewBinding>(
val fragment: Fragment,
val viewBindingFactory: (View) -> T
) : ReadOnlyProperty<Fragment, T> {
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 <T : ViewBinding> Fragment.viewBinding(viewBindingFactory: (View) -> T) =
FragmentViewBindingDelegate(this, viewBindingFactory)

View file

@ -52,7 +52,7 @@ public final class ViewDataUtils {
.setSensitive(visibleStatus.getSensitive()) .setSensitive(visibleStatus.getSensitive())
.setIsShowingSensitiveContent(alwaysShowSensitiveMedia || !visibleStatus.getSensitive()) .setIsShowingSensitiveContent(alwaysShowSensitiveMedia || !visibleStatus.getSensitive())
.setSpoilerText(visibleStatus.getSpoilerText()) .setSpoilerText(visibleStatus.getSpoilerText())
.setRebloggedByUsername(status.getReblog() == null ? null : status.getAccount().getDisplayName()) .setRebloggedByUsername(status.getReblog() == null ? null : status.getAccount().getName())
.setUserFullName(visibleStatus.getAccount().getName()) .setUserFullName(visibleStatus.getAccount().getName())
.setVisibility(visibleStatus.getVisibility()) .setVisibility(visibleStatus.getVisibility())
.setSenderId(visibleStatus.getAccount().getId()) .setSenderId(visibleStatus.getAccount().getId())

View file

@ -3,14 +3,14 @@ package com.keylesspalace.tusky.view
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.Gravity import android.view.Gravity
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.databinding.ViewBackgroundMessageBinding
import com.keylesspalace.tusky.util.visible 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. * This view is used for screens with downloadable content which may fail.
@ -22,8 +22,9 @@ class BackgroundMessageView @JvmOverloads constructor(
defStyleAttr: Int = 0 defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) { ) : LinearLayout(context, attrs, defStyleAttr) {
private val binding = ViewBackgroundMessageBinding.inflate(LayoutInflater.from(context), this)
init { init {
View.inflate(context, R.layout.view_background_message, this)
gravity = Gravity.CENTER_HORIZONTAL gravity = Gravity.CENTER_HORIZONTAL
orientation = VERTICAL orientation = VERTICAL
@ -36,11 +37,14 @@ class BackgroundMessageView @JvmOverloads constructor(
* Setup image, message and button. * Setup image, message and button.
* If [clickListener] is `null` then the button will be hidden. * If [clickListener] is `null` then the button will be hidden.
*/ */
fun setup(@DrawableRes imageRes: Int, @StringRes messageRes: Int, fun setup(
clickListener: ((v: View) -> Unit)? = null) { @DrawableRes imageRes: Int,
messageTextView.setText(messageRes) @StringRes messageRes: Int,
imageView.setImageResource(imageRes) clickListener: ((v: View) -> Unit)? = null
button.setOnClickListener(clickListener) ) {
button.visible(clickListener != null) binding.messageTextView.setText(messageRes)
binding.imageView.setImageResource(imageRes)
binding.button.setOnClickListener(clickListener)
binding.button.visible(clickListener != null)
} }
} }

View file

@ -17,12 +17,13 @@ package com.keylesspalace.tusky.view
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater
import com.google.android.material.card.MaterialCardView import com.google.android.material.card.MaterialCardView
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.databinding.CardLicenseBinding
import com.keylesspalace.tusky.util.LinkHelper import com.keylesspalace.tusky.util.LinkHelper
import com.keylesspalace.tusky.util.ThemeUtils import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import kotlinx.android.synthetic.main.card_license.view.*
class LicenseCard class LicenseCard
@JvmOverloads constructor( @JvmOverloads constructor(
@ -32,7 +33,7 @@ class LicenseCard
) : MaterialCardView(context, attrs, defStyleAttr) { ) : MaterialCardView(context, attrs, defStyleAttr) {
init { init {
inflate(context, R.layout.card_license, this) val binding = CardLicenseBinding.inflate(LayoutInflater.from(context), this)
setCardBackgroundColor(ThemeUtils.getColor(context, R.attr.colorSurface)) setCardBackgroundColor(ThemeUtils.getColor(context, R.attr.colorSurface))
@ -43,12 +44,12 @@ class LicenseCard
val link: String? = a.getString(R.styleable.LicenseCard_link) val link: String? = a.getString(R.styleable.LicenseCard_link)
a.recycle() a.recycle()
licenseCardName.text = name binding.licenseCardName.text = name
licenseCardLicense.text = license binding.licenseCardLicense.text = license
if(link.isNullOrBlank()) { if(link.isNullOrBlank()) {
licenseCardLink.hide() binding.licenseCardLink.hide()
} else { } else {
licenseCardLink.text = link binding.licenseCardLink.text = link
setOnClickListener { LinkHelper.openLink(link, context) } setOnClickListener { LinkHelper.openLink(link, context) }
} }

View file

@ -3,29 +3,33 @@
package com.keylesspalace.tusky.view package com.keylesspalace.tusky.view
import android.app.Activity import android.app.Activity
import android.widget.CheckBox
import android.widget.Spinner
import android.widget.TextView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.databinding.DialogMuteAccountBinding
fun showMuteAccountDialog( fun showMuteAccountDialog(
activity: Activity, activity: Activity,
accountUsername: String, 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) val binding = DialogMuteAccountBinding.inflate(activity.layoutInflater)
(view.findViewById(R.id.warning) as TextView).text = binding.warning.text = activity.getString(R.string.dialog_mute_warning, accountUsername)
activity.getString(R.string.dialog_mute_warning, accountUsername) binding.checkbox.isChecked = true
val checkbox: CheckBox = view.findViewById(R.id.checkbox)
checkbox.isChecked = true
AlertDialog.Builder(activity) AlertDialog.Builder(activity)
.setView(view) .setView(binding.root)
.setPositiveButton(android.R.string.ok) { _, _ -> .setPositiveButton(android.R.string.ok) { _, _ ->
val spinner: Spinner = view.findViewById(R.id.duration)
val durationValues = activity.resources.getIntArray(R.array.mute_duration_values) 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) .setNegativeButton(android.R.string.cancel, null)
.show() .show()

View file

@ -3,7 +3,7 @@ package com.keylesspalace.tusky.viewdata
import android.os.Parcelable import android.os.Parcelable
import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.entity.Status
import kotlinx.android.parcel.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class AttachmentViewData( data class AttachmentViewData(

View file

@ -475,6 +475,7 @@ public abstract class StatusViewData {
application = viewData.application; application = viewData.application;
statusEmojis = viewData.getStatusEmojis(); statusEmojis = viewData.getStatusEmojis();
accountEmojis = viewData.getAccountEmojis(); accountEmojis = viewData.getAccountEmojis();
rebloggedByAccountEmojis = viewData.getRebloggedByAccountEmojis();
card = viewData.getCard(); card = viewData.getCard();
isCollapsible = viewData.isCollapsible(); isCollapsible = viewData.isCollapsible();
isCollapsed = viewData.isCollapsed(); isCollapsed = viewData.isCollapsed();

View file

@ -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) changeRelationship(RelationShipAction.MUTE, notifications, duration)
} }

View file

@ -6,7 +6,9 @@
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context="com.keylesspalace.tusky.AboutActivity"> tools:context="com.keylesspalace.tusky.AboutActivity">
<include layout="@layout/toolbar_basic" /> <include
android:id="@+id/includedToolbar"
layout="@layout/toolbar_basic" />
<FrameLayout <FrameLayout
android:layout_width="match_parent" android:layout_width="match_parent"

Some files were not shown because too many files have changed in this diff Show more