diff --git a/app/build.gradle b/app/build.gradle index 1e67b551b..3c0702d3e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -60,8 +60,7 @@ android { lint { lintConfig file("lint.xml") - // Regenerate by deleting app/lint-baseline.xml, then run: - // ./gradlew lintBlueDebug + // Regenerate by running `./gradlew app:newLintBaseline` baseline = file("lint-baseline.xml") } @@ -203,3 +202,18 @@ tasks.withType(org.jetbrains.kotlin.gradle.internal.KaptWithoutKotlincTask) { "--add-opens", "jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED", "--add-opens", "jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED"]) } + +tasks.register("newLintBaseline") { + description 'Deletes and then recreates the lint baseline' + + // This task should always run, irrespective of caching + notCompatibleWithConfigurationCache("Is always out of date") + outputs.upToDateWhen { false } + + doLast { + delete android.lint.baseline.path + } + + // Regenerate the lint baseline + it.finalizedBy tasks.named("lintBlueDebug") +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaFragment.kt index b39a8b5b4..5a81c418e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaFragment.kt @@ -82,13 +82,10 @@ class AccountMediaFragment : override fun onViewCreated(view: View, savedInstanceState: Bundle?) { requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED) - val alwaysShowSensitiveMedia = accountManager.activeAccount!!.alwaysShowSensitiveMedia - val preferences = PreferenceManager.getDefaultSharedPreferences(view.context) val useBlurhash = preferences.getBoolean(PrefKeys.USE_BLURHASH, true) adapter = AccountMediaGridAdapter( - alwaysShowSensitiveMedia = alwaysShowSensitiveMedia, useBlurhash = useBlurhash, context = view.context, onAttachmentClickListener = ::onAttachmentClick diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaGridAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaGridAdapter.kt index 48b3a21da..aecbeb0bb 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaGridAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaGridAdapter.kt @@ -24,7 +24,6 @@ import com.keylesspalace.tusky.viewdata.AttachmentViewData import java.util.Random class AccountMediaGridAdapter( - private val alwaysShowSensitiveMedia: Boolean, private val useBlurhash: Boolean, context: Context, private val onAttachmentClickListener: (AttachmentViewData, View) -> Unit @@ -80,7 +79,7 @@ class AccountMediaGridAdapter( .into(imageView) imageView.contentDescription = item.attachment.getFormattedDescription(context) - } else if (item.sensitive && !item.isRevealed && !alwaysShowSensitiveMedia) { + } else if (item.sensitive && !item.isRevealed) { overlay.show() overlay.setImageDrawable(mediaHiddenDrawable) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaRemoteMediator.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaRemoteMediator.kt index 315b0380c..52535a6a5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaRemoteMediator.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaRemoteMediator.kt @@ -20,6 +20,7 @@ import androidx.paging.LoadType import androidx.paging.PagingState import androidx.paging.RemoteMediator import com.keylesspalace.tusky.components.timeline.util.ifExpected +import com.keylesspalace.tusky.db.AccountEntity import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.viewdata.AttachmentViewData import retrofit2.HttpException @@ -27,9 +28,9 @@ import retrofit2.HttpException @OptIn(ExperimentalPagingApi::class) class AccountMediaRemoteMediator( private val api: MastodonApi, + private val activeAccount: AccountEntity, private val viewModel: AccountMediaViewModel ) : RemoteMediator() { - override suspend fun load( loadType: LoadType, state: PagingState @@ -58,7 +59,7 @@ class AccountMediaRemoteMediator( } val attachments = statuses.flatMap { status -> - AttachmentViewData.list(status) + AttachmentViewData.list(status, activeAccount.alwaysShowSensitiveMedia ?: false) } if (loadType == LoadType.REFRESH) { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaViewModel.kt index ddbcb71d0..ee5ffd011 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaViewModel.kt @@ -21,11 +21,13 @@ import androidx.paging.ExperimentalPagingApi import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.cachedIn +import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.viewdata.AttachmentViewData import javax.inject.Inject class AccountMediaViewModel @Inject constructor( + private val accountManager: AccountManager, api: MastodonApi ) : ViewModel() { @@ -35,6 +37,8 @@ class AccountMediaViewModel @Inject constructor( var currentSource: AccountMediaPagingSource? = null + val activeAccount = accountManager.activeAccount!! + @OptIn(ExperimentalPagingApi::class) val media = Pager( config = PagingConfig( @@ -48,7 +52,7 @@ class AccountMediaViewModel @Inject constructor( currentSource = source } }, - remoteMediator = AccountMediaRemoteMediator(api, this) + remoteMediator = AccountMediaRemoteMediator(api, activeAccount, this) ).flow .cachedIn(viewModelScope) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ComposeScheduleView.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ComposeScheduleView.kt index f2e00d341..2f6fad162 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ComposeScheduleView.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ComposeScheduleView.kt @@ -57,7 +57,9 @@ class ComposeScheduleView ).apply { timeZone = TimeZone.getTimeZone("UTC") } - private var scheduleDateTime: Calendar? = null + + /** The date/time the user has chosen to schedule the status, in UTC */ + private var scheduleDateTimeUtc: Calendar? = null init { binding.scheduledDateTime.setOnClickListener { openPickDateDialog() } @@ -71,13 +73,13 @@ class ComposeScheduleView } private fun updateScheduleUi() { - if (scheduleDateTime == null) { + if (scheduleDateTimeUtc == null) { binding.scheduledDateTime.text = "" binding.invalidScheduleWarning.visibility = GONE return } - val scheduled = scheduleDateTime!!.time + val scheduled = scheduleDateTimeUtc!!.time binding.scheduledDateTime.text = String.format( "%s %s", dateFormat.format(scheduled), @@ -98,21 +100,37 @@ class ComposeScheduleView } fun resetSchedule() { - scheduleDateTime = null + scheduleDateTimeUtc = null updateScheduleUi() } fun openPickDateDialog() { - val yesterday = Calendar.getInstance().timeInMillis - 24 * 60 * 60 * 1000 + // The earliest point in time the calendar should display. Start with current date/time + val earliest = calendar().apply { + // Add the minimum scheduling interval. This may roll the calendar over to the + // next day (e.g. if the current time is 23:57). + add(Calendar.SECOND, MINIMUM_SCHEDULED_SECONDS) + // Clear out the time components, so it's midnight + set(Calendar.HOUR_OF_DAY, 0) + set(Calendar.MINUTE, 0) + set(Calendar.SECOND, 0) + set(Calendar.MILLISECOND, 0) + } val calendarConstraints = CalendarConstraints.Builder() - .setValidator( - DateValidatorPointForward.from(yesterday) - ) + .setValidator(DateValidatorPointForward.from(earliest.timeInMillis)) .build() initializeSuggestedTime() + + // Work around a misfeature in MaterialDatePicker. The `selection` is treated as + // millis-from-epoch, in UTC, which is good. However, it is also *displayed* in UTC + // instead of converting to the user's local timezone. + // + // So we have to add the TZ offset before setting it in the picker + val tzOffset = TimeZone.getDefault().getOffset(scheduleDateTimeUtc!!.timeInMillis) + val picker = MaterialDatePicker.Builder .datePicker() - .setSelection(scheduleDateTime!!.timeInMillis) + .setSelection(scheduleDateTimeUtc!!.timeInMillis + tzOffset) .setCalendarConstraints(calendarConstraints) .build() picker.addOnPositiveButtonClickListener { selection: Long -> onDateSet(selection) } @@ -129,11 +147,12 @@ class ComposeScheduleView private fun openPickTimeDialog() { val pickerBuilder = MaterialTimePicker.Builder() - scheduleDateTime?.let { + scheduleDateTimeUtc?.let { pickerBuilder.setHour(it[Calendar.HOUR_OF_DAY]) .setMinute(it[Calendar.MINUTE]) } + pickerBuilder.setTitleText(dateFormat.format(scheduleDateTimeUtc!!.timeInMillis)) pickerBuilder.setTimeFormat(getTimeFormat(context)) val picker = pickerBuilder.build() @@ -154,7 +173,7 @@ class ComposeScheduleView fun setDateTime(scheduledAt: String?) { val date = getDateTime(scheduledAt) ?: return initializeSuggestedTime() - scheduleDateTime!!.time = date + scheduleDateTimeUtc!!.time = date updateScheduleUi() } @@ -180,24 +199,24 @@ class ComposeScheduleView // see https://github.com/material-components/material-components-android/issues/882 newDate.timeZone = TimeZone.getTimeZone("UTC") newDate.timeInMillis = selection - scheduleDateTime!![newDate[Calendar.YEAR], newDate[Calendar.MONTH]] = newDate[Calendar.DATE] + scheduleDateTimeUtc!![newDate[Calendar.YEAR], newDate[Calendar.MONTH]] = newDate[Calendar.DATE] openPickTimeDialog() } private fun onTimeSet(hourOfDay: Int, minute: Int) { initializeSuggestedTime() - scheduleDateTime?.set(Calendar.HOUR_OF_DAY, hourOfDay) - scheduleDateTime?.set(Calendar.MINUTE, minute) + scheduleDateTimeUtc?.set(Calendar.HOUR_OF_DAY, hourOfDay) + scheduleDateTimeUtc?.set(Calendar.MINUTE, minute) updateScheduleUi() listener?.onTimeSet(time) } val time: String? - get() = scheduleDateTime?.time?.let { iso8601.format(it) } + get() = scheduleDateTimeUtc?.time?.let { iso8601.format(it) } private fun initializeSuggestedTime() { - if (scheduleDateTime == null) { - scheduleDateTime = calendar().apply { + if (scheduleDateTimeUtc == null) { + scheduleDateTimeUtc = calendar().apply { add(Calendar.MINUTE, 15) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsFragment.kt index a80fd96aa..f26a46528 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsFragment.kt @@ -518,7 +518,11 @@ class NotificationsFragment : override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) { val status = adapter.peek(position)?.statusViewData?.status ?: return - super.viewMedia(attachmentIndex, list(status), view) + super.viewMedia( + attachmentIndex, + list(status, viewModel.statusDisplayOptions.value.showSensitiveMedia), + view + ) } override fun onViewThread(position: Int) { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadFragment.kt index 0d4ba3b35..d7541f428 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadFragment.kt @@ -336,7 +336,11 @@ class ViewThreadFragment : override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) { val status = adapter.currentList[position].status - super.viewMedia(attachmentIndex, list(status), view) + super.viewMedia( + attachmentIndex, + list(status, alwaysShowSensitiveMedia), + view + ) } override fun onViewThread(position: Int) { diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/AttachmentViewData.kt b/app/src/main/java/com/keylesspalace/tusky/viewdata/AttachmentViewData.kt index ae24cebe1..998cc96a4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewdata/AttachmentViewData.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/AttachmentViewData.kt @@ -35,7 +35,7 @@ data class AttachmentViewData( companion object { @JvmStatic - fun list(status: Status): List { + fun list(status: Status, alwaysShowSensitiveMedia: Boolean = false): List { val actionable = status.actionableStatus return actionable.attachments.map { attachment -> AttachmentViewData( @@ -43,7 +43,7 @@ data class AttachmentViewData( statusId = actionable.id, statusUrl = actionable.url!!, sensitive = actionable.sensitive, - isRevealed = !actionable.sensitive + isRevealed = alwaysShowSensitiveMedia || !actionable.sensitive ) } } diff --git a/app/src/main/res/layout/item_status_detailed.xml b/app/src/main/res/layout/item_status_detailed.xml index cc6e4de2e..b9f64bd3b 100644 --- a/app/src/main/res/layout/item_status_detailed.xml +++ b/app/src/main/res/layout/item_status_detailed.xml @@ -89,6 +89,7 @@ android:importantForAccessibility="no" android:lineSpacingMultiplier="1.1" android:textColor="?android:textColorPrimary" + android:textIsSelectable="true" android:textSize="?attr/status_text_large" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e90e5c72b..447d295b7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -60,7 +60,7 @@ google-ksp = "com.google.devtools.ksp:1.9.0-1.0.13" kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" } kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } -ktlint = "org.jlleitschuh.gradle.ktlint:11.5.0" +ktlint = "org.jlleitschuh.gradle.ktlint:11.5.1" [libraries] android-material = { module = "com.google.android.material:material", version.ref = "material" }