Merge branch 'develop' into refactor_instancemute
This commit is contained in:
commit
a96460cb16
252 changed files with 5810 additions and 2827 deletions
|
|
@ -772,13 +772,16 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
|
|||
loadedAccount?.let { loadedAccount ->
|
||||
val muteDomain = menu.findItem(R.id.action_mute_domain)
|
||||
domain = getDomain(loadedAccount.url)
|
||||
if (domain.isEmpty()) {
|
||||
when {
|
||||
// If we can't get the domain, there's no way we can mute it anyway...
|
||||
menu.removeItem(R.id.action_mute_domain)
|
||||
} else {
|
||||
if (blockingDomain) {
|
||||
// If the account is from our own domain, muting it is no-op
|
||||
domain.isEmpty() || viewModel.isFromOwnDomain -> {
|
||||
menu.removeItem(R.id.action_mute_domain)
|
||||
}
|
||||
blockingDomain -> {
|
||||
muteDomain.title = getString(R.string.action_unmute_domain, domain)
|
||||
} else {
|
||||
}
|
||||
else -> {
|
||||
muteDomain.title = getString(R.string.action_mute_domain, domain)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import com.keylesspalace.tusky.util.Error
|
|||
import com.keylesspalace.tusky.util.Loading
|
||||
import com.keylesspalace.tusky.util.Resource
|
||||
import com.keylesspalace.tusky.util.Success
|
||||
import com.keylesspalace.tusky.util.getDomain
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -27,7 +28,7 @@ import javax.inject.Inject
|
|||
class AccountViewModel @Inject constructor(
|
||||
private val mastodonApi: MastodonApi,
|
||||
private val eventHub: EventHub,
|
||||
private val accountManager: AccountManager
|
||||
accountManager: AccountManager
|
||||
) : ViewModel() {
|
||||
|
||||
val accountData = MutableLiveData<Resource<Account>>()
|
||||
|
|
@ -41,8 +42,13 @@ class AccountViewModel @Inject constructor(
|
|||
lateinit var accountId: String
|
||||
var isSelf = false
|
||||
|
||||
/** True if the viewed account has the same domain as the active account */
|
||||
var isFromOwnDomain = false
|
||||
|
||||
private var noteUpdateJob: Job? = null
|
||||
|
||||
private val activeAccount = accountManager.activeAccount!!
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
eventHub.events.collect { event ->
|
||||
|
|
@ -65,6 +71,8 @@ class AccountViewModel @Inject constructor(
|
|||
accountData.postValue(Success(account))
|
||||
isDataLoading = false
|
||||
isRefreshing.postValue(false)
|
||||
|
||||
isFromOwnDomain = getDomain(account.url) == activeAccount.domain
|
||||
},
|
||||
{ t ->
|
||||
Log.w(TAG, "failed obtaining account", t)
|
||||
|
|
@ -298,7 +306,7 @@ class AccountViewModel @Inject constructor(
|
|||
|
||||
fun setAccountInfo(accountId: String) {
|
||||
this.accountId = accountId
|
||||
this.isSelf = accountManager.activeAccount?.accountId == accountId
|
||||
this.isSelf = activeAccount.accountId == accountId
|
||||
reload(false)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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<String, AttachmentViewData>() {
|
||||
|
||||
override suspend fun load(
|
||||
loadType: LoadType,
|
||||
state: PagingState<String, AttachmentViewData>
|
||||
|
|
@ -58,7 +59,7 @@ class AccountMediaRemoteMediator(
|
|||
}
|
||||
|
||||
val attachments = statuses.flatMap { status ->
|
||||
AttachmentViewData.list(status)
|
||||
AttachmentViewData.list(status, activeAccount.alwaysShowSensitiveMedia ?: false)
|
||||
}
|
||||
|
||||
if (loadType == LoadType.REFRESH) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,6 @@ class AccountListActivity : BottomSheetActivity(), HasAndroidInjector {
|
|||
|
||||
val type = intent.getSerializableExtra(EXTRA_TYPE) as Type
|
||||
val id: String? = intent.getStringExtra(EXTRA_ID)
|
||||
val accountLocked: Boolean = intent.getBooleanExtra(EXTRA_ACCOUNT_LOCKED, false)
|
||||
|
||||
setSupportActionBar(binding.includedToolbar.toolbar)
|
||||
supportActionBar?.apply {
|
||||
|
|
@ -66,7 +65,7 @@ class AccountListActivity : BottomSheetActivity(), HasAndroidInjector {
|
|||
}
|
||||
|
||||
supportFragmentManager.commit {
|
||||
replace(R.id.fragment_container, AccountListFragment.newInstance(type, id, accountLocked))
|
||||
replace(R.id.fragment_container, AccountListFragment.newInstance(type, id))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -75,13 +74,11 @@ class AccountListActivity : BottomSheetActivity(), HasAndroidInjector {
|
|||
companion object {
|
||||
private const val EXTRA_TYPE = "type"
|
||||
private const val EXTRA_ID = "id"
|
||||
private const val EXTRA_ACCOUNT_LOCKED = "acc_locked"
|
||||
|
||||
fun newIntent(context: Context, type: Type, id: String? = null, accountLocked: Boolean = false): Intent {
|
||||
fun newIntent(context: Context, type: Type, id: String? = null): Intent {
|
||||
return Intent(context, AccountListActivity::class.java).apply {
|
||||
putExtra(EXTRA_TYPE, type)
|
||||
putExtra(EXTRA_ID, id)
|
||||
putExtra(EXTRA_ACCOUNT_LOCKED, accountLocked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,7 +61,6 @@ import com.keylesspalace.tusky.view.EndlessOnScrollListener
|
|||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import kotlinx.coroutines.launch
|
||||
import retrofit2.Response
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
class AccountListFragment :
|
||||
|
|
@ -107,13 +106,15 @@ class AccountListFragment :
|
|||
val animateEmojis = pm.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
|
||||
val showBotOverlay = pm.getBoolean(PrefKeys.SHOW_BOT_OVERLAY, true)
|
||||
|
||||
val activeAccount = accountManager.activeAccount!!
|
||||
|
||||
adapter = when (type) {
|
||||
Type.BLOCKS -> BlocksAdapter(this, animateAvatar, animateEmojis, showBotOverlay)
|
||||
Type.MUTES -> MutesAdapter(this, animateAvatar, animateEmojis, showBotOverlay)
|
||||
Type.FOLLOW_REQUESTS -> {
|
||||
val headerAdapter = FollowRequestsHeaderAdapter(
|
||||
instanceName = accountManager.activeAccount!!.domain,
|
||||
accountLocked = arguments?.getBoolean(ARG_ACCOUNT_LOCKED) == true
|
||||
instanceName = activeAccount.domain,
|
||||
accountLocked = activeAccount.locked
|
||||
)
|
||||
val followRequestsAdapter = FollowRequestsAdapter(this, this, animateAvatar, animateEmojis, showBotOverlay)
|
||||
binding.recyclerView.adapter = ConcatAdapter(headerAdapter, followRequestsAdapter)
|
||||
|
|
@ -330,7 +331,7 @@ class AccountListFragment :
|
|||
|
||||
val linkHeader = response.headers()["Link"]
|
||||
onFetchAccountsSuccess(accountList, linkHeader)
|
||||
} catch (exception: IOException) {
|
||||
} catch (exception: Exception) {
|
||||
onFetchAccountsFailure(exception)
|
||||
}
|
||||
}
|
||||
|
|
@ -404,14 +405,12 @@ class AccountListFragment :
|
|||
private const val TAG = "AccountList" // logging tag
|
||||
private const val ARG_TYPE = "type"
|
||||
private const val ARG_ID = "id"
|
||||
private const val ARG_ACCOUNT_LOCKED = "acc_locked"
|
||||
|
||||
fun newInstance(type: Type, id: String? = null, accountLocked: Boolean = false): AccountListFragment {
|
||||
fun newInstance(type: Type, id: String? = null): AccountListFragment {
|
||||
return AccountListFragment().apply {
|
||||
arguments = Bundle(3).apply {
|
||||
putSerializable(ARG_TYPE, type)
|
||||
putString(ARG_ID, id)
|
||||
putBoolean(ARG_ACCOUNT_LOCKED, accountLocked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ class AnnouncementsActivity :
|
|||
is Error -> {
|
||||
binding.progressBar.hide()
|
||||
binding.swipeRefreshLayout.isRefreshing = false
|
||||
binding.errorMessageView.setup(R.drawable.elephant_error, R.string.error_generic) {
|
||||
binding.errorMessageView.setup(R.drawable.errorphant_error, R.string.error_generic) {
|
||||
refreshAnnouncements()
|
||||
}
|
||||
binding.errorMessageView.show()
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
package com.keylesspalace.tusky.components.compose
|
||||
|
||||
import android.Manifest
|
||||
import android.app.NotificationManager
|
||||
import android.app.ProgressDialog
|
||||
import android.content.ClipData
|
||||
import android.content.Context
|
||||
|
|
@ -95,6 +94,7 @@ import com.keylesspalace.tusky.entity.Emoji
|
|||
import com.keylesspalace.tusky.entity.NewPoll
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.settings.PrefKeys
|
||||
import com.keylesspalace.tusky.settings.PrefKeys.APP_THEME
|
||||
import com.keylesspalace.tusky.util.APP_THEME_DEFAULT
|
||||
import com.keylesspalace.tusky.util.MentionSpan
|
||||
import com.keylesspalace.tusky.util.PickMediaFiles
|
||||
|
|
@ -207,24 +207,9 @@ class ComposeActivity :
|
|||
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val notificationId = intent.getIntExtra(NOTIFICATION_ID_EXTRA, -1)
|
||||
if (notificationId != -1) {
|
||||
// ComposeActivity was opened from a notification, delete the notification
|
||||
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||
notificationManager.cancel(notificationId)
|
||||
}
|
||||
activeAccount = accountManager.activeAccount ?: return
|
||||
|
||||
// If started from an intent then compose as the account ID from the intent.
|
||||
// Otherwise use the active account. If null then the user is not logged in,
|
||||
// and return from the activity.
|
||||
val intentAccountId = intent.getLongExtra(ACCOUNT_ID_EXTRA, -1)
|
||||
activeAccount = if (intentAccountId != -1L) {
|
||||
accountManager.getAccountById(intentAccountId)
|
||||
} else {
|
||||
accountManager.activeAccount
|
||||
} ?: return
|
||||
|
||||
val theme = preferences.getString("appTheme", APP_THEME_DEFAULT)
|
||||
val theme = preferences.getString(APP_THEME, APP_THEME_DEFAULT)
|
||||
if (theme == "black") {
|
||||
setTheme(R.style.TuskyDialogActivityBlackTheme)
|
||||
}
|
||||
|
|
@ -280,7 +265,7 @@ class ComposeActivity :
|
|||
binding.composeScheduleView.setDateTime(composeOptions?.scheduledAt)
|
||||
}
|
||||
|
||||
setupLanguageSpinner(getInitialLanguages(composeOptions?.language, accountManager.activeAccount))
|
||||
setupLanguageSpinner(getInitialLanguages(composeOptions?.language, activeAccount))
|
||||
setupComposeField(preferences, viewModel.startingText)
|
||||
setupContentWarningField(composeOptions?.contentWarning)
|
||||
setupPollView()
|
||||
|
|
@ -485,7 +470,12 @@ class ComposeActivity :
|
|||
if (throwable is UploadServerError) {
|
||||
displayTransientMessage(throwable.errorMessage)
|
||||
} else {
|
||||
displayTransientMessage(R.string.error_media_upload_sending)
|
||||
displayTransientMessage(
|
||||
getString(
|
||||
R.string.error_media_upload_sending_fmt,
|
||||
throwable.message
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -943,7 +933,10 @@ class ComposeActivity :
|
|||
val split = contentInfo.partition { item: ClipData.Item -> item.uri != null }
|
||||
split.first?.let { content ->
|
||||
for (i in 0 until content.clip.itemCount) {
|
||||
pickMedia(content.clip.getItemAt(i).uri)
|
||||
pickMedia(
|
||||
content.clip.getItemAt(i).uri,
|
||||
contentInfo.clip.description.label as String?
|
||||
)
|
||||
}
|
||||
}
|
||||
return split.second
|
||||
|
|
@ -1064,9 +1057,9 @@ class ComposeActivity :
|
|||
viewModel.removeMediaFromQueue(item)
|
||||
}
|
||||
|
||||
private fun pickMedia(uri: Uri) {
|
||||
private fun pickMedia(uri: Uri, description: String? = null) {
|
||||
lifecycleScope.launch {
|
||||
viewModel.pickMedia(uri).onFailure { throwable ->
|
||||
viewModel.pickMedia(uri, description).onFailure { throwable ->
|
||||
val errorString = when (throwable) {
|
||||
is FileSizeException -> {
|
||||
val decimalFormat = DecimalFormat("0.##")
|
||||
|
|
@ -1347,8 +1340,6 @@ class ComposeActivity :
|
|||
private const val PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1
|
||||
|
||||
internal const val COMPOSE_OPTIONS_EXTRA = "COMPOSE_OPTIONS"
|
||||
private const val NOTIFICATION_ID_EXTRA = "NOTIFICATION_ID"
|
||||
private const val ACCOUNT_ID_EXTRA = "ACCOUNT_ID"
|
||||
private const val PHOTO_UPLOAD_URI_KEY = "PHOTO_UPLOAD_URI"
|
||||
private const val VISIBILITY_KEY = "VISIBILITY"
|
||||
private const val SCHEDULED_TIME_KEY = "SCHEDULE"
|
||||
|
|
@ -1356,26 +1347,15 @@ class ComposeActivity :
|
|||
|
||||
/**
|
||||
* @param options ComposeOptions to configure the ComposeActivity
|
||||
* @param notificationId the id of the notification that starts the Activity
|
||||
* @param accountId the id of the account to compose with, null for the current account
|
||||
* @return an Intent to start the ComposeActivity
|
||||
*/
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
fun startIntent(
|
||||
context: Context,
|
||||
options: ComposeOptions,
|
||||
notificationId: Int? = null,
|
||||
accountId: Long? = null
|
||||
options: ComposeOptions
|
||||
): Intent {
|
||||
return Intent(context, ComposeActivity::class.java).apply {
|
||||
putExtra(COMPOSE_OPTIONS_EXTRA, options)
|
||||
if (notificationId != null) {
|
||||
putExtra(NOTIFICATION_ID_EXTRA, notificationId)
|
||||
}
|
||||
if (accountId != null) {
|
||||
putExtra(ACCOUNT_ID_EXTRA, accountId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@ import com.keylesspalace.tusky.service.ServiceClient
|
|||
import com.keylesspalace.tusky.service.StatusToSend
|
||||
import com.keylesspalace.tusky.util.randomAlphanumericString
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
|
@ -53,7 +52,6 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
class ComposeViewModel @Inject constructor(
|
||||
private val api: MastodonApi,
|
||||
private val accountManager: AccountManager,
|
||||
|
|
@ -95,7 +93,7 @@ class ComposeViewModel @Inject constructor(
|
|||
val media: MutableStateFlow<List<QueuedMedia>> = MutableStateFlow(emptyList())
|
||||
val uploadError = MutableSharedFlow<Throwable>(replay = 0, extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
|
||||
lateinit var composeKind: ComposeKind
|
||||
private lateinit var composeKind: ComposeKind
|
||||
|
||||
// Used in ComposeActivity to pass state to result function when cropImage contract inflight
|
||||
var cropImageItemOld: QueuedMedia? = null
|
||||
|
|
|
|||
|
|
@ -75,10 +75,6 @@ class AddPollOptionsAdapter(
|
|||
}
|
||||
|
||||
private fun validateInput(): Boolean {
|
||||
if (options.contains("") || options.distinct().size != options.size) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
return !(options.contains("") || options.distinct().size != options.size)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ class CaptionDialog : DialogFragment() {
|
|||
|
||||
input = binding.imageDescriptionText
|
||||
val imageView = binding.imageDescriptionView
|
||||
imageView.maximumScale = 6f
|
||||
imageView.maxZoom = 6f
|
||||
|
||||
input.hint = resources.getQuantityString(
|
||||
R.plurals.hint_describe_for_visually_impaired,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
|
|||
ImageView avatarView = avatars[i];
|
||||
if (i < accounts.size()) {
|
||||
ImageLoadingHelper.loadAvatar(accounts.get(i).getAvatar(), avatarView,
|
||||
avatarRadius48dp, statusDisplayOptions.animateAvatars());
|
||||
avatarRadius48dp, statusDisplayOptions.animateAvatars(), null);
|
||||
avatarView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
avatarView.setVisibility(View.GONE);
|
||||
|
|
|
|||
|
|
@ -134,6 +134,7 @@ class ConversationsFragment :
|
|||
if (loadState.append is LoadState.NotLoading && loadState.source.refresh is LoadState.NotLoading) {
|
||||
binding.statusView.show()
|
||||
binding.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null)
|
||||
binding.statusView.showHelp(R.string.help_empty_conversations)
|
||||
}
|
||||
}
|
||||
is LoadState.Error -> {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.keylesspalace.tusky.components.filters
|
||||
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface.BUTTON_POSITIVE
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.AdapterView
|
||||
|
|
@ -81,7 +82,11 @@ class EditFilterActivity : BaseActivity() {
|
|||
|
||||
binding.actionChip.setOnClickListener { showAddKeywordDialog() }
|
||||
binding.filterSaveButton.setOnClickListener { saveChanges() }
|
||||
binding.filterDeleteButton.setOnClickListener { deleteFilter() }
|
||||
binding.filterDeleteButton.setOnClickListener {
|
||||
lifecycleScope.launch {
|
||||
if (showDeleteFilterDialog(filter.title) == BUTTON_POSITIVE) deleteFilter()
|
||||
}
|
||||
}
|
||||
binding.filterDeleteButton.visible(originalFilter != null)
|
||||
|
||||
for (switch in contextSwitches.keys) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright 2023 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.filters
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.util.await
|
||||
|
||||
internal suspend fun Activity.showDeleteFilterDialog(filterTitle: String) = AlertDialog.Builder(this)
|
||||
.setMessage(getString(R.string.dialog_delete_filter_text, filterTitle))
|
||||
.setCancelable(true)
|
||||
.create()
|
||||
.await(R.string.dialog_delete_filter_positive_action, android.R.string.cancel)
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.keylesspalace.tusky.components.filters
|
||||
|
||||
import android.content.DialogInterface.BUTTON_POSITIVE
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.viewModels
|
||||
|
|
@ -60,18 +61,19 @@ class FiltersActivity : BaseActivity(), FiltersListener {
|
|||
when (state.loadingState) {
|
||||
FiltersViewModel.LoadingState.INITIAL, FiltersViewModel.LoadingState.LOADING -> binding.messageView.hide()
|
||||
FiltersViewModel.LoadingState.ERROR_NETWORK -> {
|
||||
binding.messageView.setup(R.drawable.elephant_offline, R.string.error_network) {
|
||||
binding.messageView.setup(R.drawable.errorphant_offline, R.string.error_network) {
|
||||
loadFilters()
|
||||
}
|
||||
binding.messageView.show()
|
||||
}
|
||||
FiltersViewModel.LoadingState.ERROR_OTHER -> {
|
||||
binding.messageView.setup(R.drawable.elephant_error, R.string.error_generic) {
|
||||
binding.messageView.setup(R.drawable.errorphant_error, R.string.error_generic) {
|
||||
loadFilters()
|
||||
}
|
||||
binding.messageView.show()
|
||||
}
|
||||
FiltersViewModel.LoadingState.LOADED -> {
|
||||
binding.filtersList.adapter = FiltersAdapter(this@FiltersActivity, state.filters)
|
||||
if (state.filters.isEmpty()) {
|
||||
binding.messageView.setup(
|
||||
R.drawable.elephant_friend_empty,
|
||||
|
|
@ -81,7 +83,6 @@ class FiltersActivity : BaseActivity(), FiltersListener {
|
|||
binding.messageView.show()
|
||||
} else {
|
||||
binding.messageView.hide()
|
||||
binding.filtersList.adapter = FiltersAdapter(this@FiltersActivity, state.filters)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -104,7 +105,11 @@ class FiltersActivity : BaseActivity(), FiltersListener {
|
|||
}
|
||||
|
||||
override fun deleteFilter(filter: Filter) {
|
||||
viewModel.deleteFilter(filter, binding.root)
|
||||
lifecycleScope.launch {
|
||||
if (showDeleteFilterDialog(filter.title) == BUTTON_POSITIVE) {
|
||||
viewModel.deleteFilter(filter, binding.root)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateFilter(updatedFilter: Filter) {
|
||||
|
|
|
|||
|
|
@ -28,5 +28,6 @@ data class InstanceInfo(
|
|||
val maxMediaAttachments: Int,
|
||||
val maxFields: Int,
|
||||
val maxFieldNameLength: Int?,
|
||||
val maxFieldValueLength: Int?
|
||||
val maxFieldValueLength: Int?,
|
||||
val version: String?
|
||||
)
|
||||
|
|
|
|||
|
|
@ -99,7 +99,8 @@ class InstanceInfoRepository @Inject constructor(
|
|||
maxMediaAttachments = instanceInfo?.maxMediaAttachments ?: DEFAULT_MAX_MEDIA_ATTACHMENTS,
|
||||
maxFields = instanceInfo?.maxFields ?: DEFAULT_MAX_ACCOUNT_FIELDS,
|
||||
maxFieldNameLength = instanceInfo?.maxFieldNameLength,
|
||||
maxFieldValueLength = instanceInfo?.maxFieldValueLength
|
||||
maxFieldValueLength = instanceInfo?.maxFieldValueLength,
|
||||
version = instanceInfo?.version
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ sealed class LoginResult : Parcelable {
|
|||
data class Err(val errorMessage: String) : LoginResult()
|
||||
|
||||
@Parcelize
|
||||
object Cancel : LoginResult()
|
||||
data object Cancel : LoginResult()
|
||||
}
|
||||
|
||||
/** Activity to do Oauth process using WebView. */
|
||||
|
|
|
|||
|
|
@ -85,13 +85,6 @@ public class NotificationHelper {
|
|||
/** Dynamic notification IDs start here */
|
||||
private static int notificationId = NOTIFICATION_ID_PRUNE_CACHE + 1;
|
||||
|
||||
/**
|
||||
* constants used in Intents
|
||||
*/
|
||||
public static final String ACCOUNT_ID = "account_id";
|
||||
|
||||
public static final String TYPE = APPLICATION_ID + ".notification.type";
|
||||
|
||||
private static final String TAG = "NotificationHelper";
|
||||
|
||||
public static final String REPLY_ACTION = "REPLY_ACTION";
|
||||
|
|
@ -245,7 +238,7 @@ public class NotificationHelper {
|
|||
Bundle extras = new Bundle();
|
||||
// Add the sending account's name, so it can be used when summarising this notification
|
||||
extras.putString(EXTRA_ACCOUNT_NAME, body.getAccount().getName());
|
||||
extras.putString(EXTRA_NOTIFICATION_TYPE, body.getType().toString());
|
||||
extras.putSerializable(EXTRA_NOTIFICATION_TYPE, body.getType());
|
||||
builder.addExtras(extras);
|
||||
|
||||
// Only alert for the first notification of a batch to avoid multiple alerts at once
|
||||
|
|
@ -285,7 +278,7 @@ public class NotificationHelper {
|
|||
int accountId = (int) account.getId();
|
||||
|
||||
// Initialise the map with all channel IDs.
|
||||
for (Notification.Type ty : Notification.Type.values()) {
|
||||
for (Notification.Type ty : Notification.Type.getEntries()) {
|
||||
channelGroups.put(getChannelId(account, ty), new ArrayList<>());
|
||||
}
|
||||
|
||||
|
|
@ -325,11 +318,10 @@ public class NotificationHelper {
|
|||
// Create a notification that summarises the other notifications in this group
|
||||
|
||||
// All notifications in this group have the same type, so get it from the first.
|
||||
String notificationType = members.get(0).getNotification().extras.getString(EXTRA_NOTIFICATION_TYPE);
|
||||
Notification.Type notificationType = (Notification.Type)members.get(0).getNotification().extras.getSerializable(EXTRA_NOTIFICATION_TYPE);
|
||||
|
||||
Intent summaryResultIntent = MainActivity.openNotificationIntent(context, accountId, notificationType);
|
||||
|
||||
Intent summaryResultIntent = new Intent(context, MainActivity.class);
|
||||
summaryResultIntent.putExtra(ACCOUNT_ID, (long) accountId);
|
||||
summaryResultIntent.putExtra(TYPE, notificationType);
|
||||
TaskStackBuilder summaryStackBuilder = TaskStackBuilder.create(context);
|
||||
summaryStackBuilder.addParentStack(MainActivity.class);
|
||||
summaryStackBuilder.addNextIntent(summaryResultIntent);
|
||||
|
|
@ -373,10 +365,8 @@ public class NotificationHelper {
|
|||
|
||||
private static NotificationCompat.Builder newAndroidNotification(Context context, Notification body, AccountEntity account) {
|
||||
|
||||
// we have to switch account here
|
||||
Intent eventResultIntent = new Intent(context, MainActivity.class);
|
||||
eventResultIntent.putExtra(ACCOUNT_ID, account.getId());
|
||||
eventResultIntent.putExtra(TYPE, body.getType().name());
|
||||
Intent eventResultIntent = MainActivity.openNotificationIntent(context, account.getId(), body.getType());
|
||||
|
||||
TaskStackBuilder eventStackBuilder = TaskStackBuilder.create(context);
|
||||
eventStackBuilder.addParentStack(MainActivity.class);
|
||||
eventStackBuilder.addNextIntent(eventResultIntent);
|
||||
|
|
@ -464,12 +454,7 @@ public class NotificationHelper {
|
|||
composeOptions.setLanguage(actionableStatus.getLanguage());
|
||||
composeOptions.setKind(ComposeActivity.ComposeKind.NEW);
|
||||
|
||||
Intent composeIntent = ComposeActivity.startIntent(
|
||||
context,
|
||||
composeOptions,
|
||||
notificationId,
|
||||
account.getId()
|
||||
);
|
||||
Intent composeIntent = MainActivity.composeIntent(context, composeOptions, account.getId(), body.getId(), (int)account.getId());
|
||||
|
||||
composeIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ import android.view.MenuItem
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.DialogFragment
|
||||
|
|
@ -46,7 +45,6 @@ import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
|
|||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
|
||||
import at.connyduck.sparkbutton.helpers.Utils
|
||||
import com.google.android.material.appbar.AppBarLayout.ScrollingViewBehavior
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.keylesspalace.tusky.R
|
||||
|
|
@ -123,21 +121,6 @@ class NotificationsFragment :
|
|||
return inflater.inflate(R.layout.fragment_timeline_notifications, container, false)
|
||||
}
|
||||
|
||||
private fun updateFilterVisibility(showFilter: Boolean) {
|
||||
val params = binding.swipeRefreshLayout.layoutParams as CoordinatorLayout.LayoutParams
|
||||
if (showFilter) {
|
||||
binding.appBarOptions.setExpanded(true, false)
|
||||
binding.appBarOptions.visibility = View.VISIBLE
|
||||
// Set content behaviour to hide filter on scroll
|
||||
params.behavior = ScrollingViewBehavior()
|
||||
} else {
|
||||
binding.appBarOptions.setExpanded(false, false)
|
||||
binding.appBarOptions.visibility = View.GONE
|
||||
// Clear behaviour to hide app bar
|
||||
params.behavior = null
|
||||
}
|
||||
}
|
||||
|
||||
private fun confirmClearNotifications() {
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setMessage(R.string.notification_clear_text)
|
||||
|
|
@ -215,8 +198,6 @@ class NotificationsFragment :
|
|||
footer = NotificationsLoadStateAdapter { adapter.retry() }
|
||||
)
|
||||
|
||||
binding.buttonClear.setOnClickListener { confirmClearNotifications() }
|
||||
binding.buttonFilter.setOnClickListener { showFilterDialog() }
|
||||
(binding.recyclerView.itemAnimator as SimpleItemAnimator?)!!.supportsChangeAnimations =
|
||||
false
|
||||
|
||||
|
|
@ -293,7 +274,7 @@ class NotificationsFragment :
|
|||
val position = adapter.snapshot().indexOfFirst {
|
||||
it?.statusViewData?.status?.id == (action as StatusAction).statusViewData.id
|
||||
}
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
if (position != NO_POSITION) {
|
||||
adapter.notifyItemChanged(position)
|
||||
}
|
||||
}
|
||||
|
|
@ -369,10 +350,10 @@ class NotificationsFragment :
|
|||
}
|
||||
}
|
||||
|
||||
// Update filter option visibility from uiState
|
||||
launch {
|
||||
viewModel.uiState.collectLatest { updateFilterVisibility(it.showFilterOptions) }
|
||||
}
|
||||
// Collect the uiState. Nothing is done with it, but if you don't collect it then
|
||||
// accessing viewModel.uiState.value (e.g., when the filter dialog is created)
|
||||
// returns an empty object.
|
||||
launch { viewModel.uiState.collect() }
|
||||
|
||||
// Update status display from statusDisplayOptions. If the new options request
|
||||
// relative time display collect the flow to periodically update the timestamp in the list gui elements.
|
||||
|
|
@ -418,13 +399,13 @@ class NotificationsFragment :
|
|||
when ((loadState.refresh as LoadState.Error).error) {
|
||||
is IOException -> {
|
||||
binding.statusView.setup(
|
||||
R.drawable.elephant_offline,
|
||||
R.drawable.errorphant_offline,
|
||||
R.string.error_network
|
||||
) { adapter.retry() }
|
||||
}
|
||||
else -> {
|
||||
binding.statusView.setup(
|
||||
R.drawable.elephant_error,
|
||||
R.drawable.errorphant_error,
|
||||
R.string.error_generic
|
||||
) { adapter.retry() }
|
||||
}
|
||||
|
|
@ -439,10 +420,17 @@ class NotificationsFragment :
|
|||
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
menuInflater.inflate(R.menu.fragment_notifications, menu)
|
||||
val iconColor = MaterialColors.getColor(binding.root, android.R.attr.textColorPrimary)
|
||||
menu.findItem(R.id.action_refresh)?.apply {
|
||||
icon = IconicsDrawable(requireContext(), GoogleMaterial.Icon.gmd_refresh).apply {
|
||||
sizeDp = 20
|
||||
colorInt = MaterialColors.getColor(binding.root, android.R.attr.textColorPrimary)
|
||||
colorInt = iconColor
|
||||
}
|
||||
}
|
||||
menu.findItem(R.id.action_edit_notification_filter)?.apply {
|
||||
icon = IconicsDrawable(requireContext(), GoogleMaterial.Icon.gmd_tune).apply {
|
||||
sizeDp = 20
|
||||
colorInt = iconColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -458,6 +446,14 @@ class NotificationsFragment :
|
|||
viewModel.accept(InfallibleUiAction.LoadNewest)
|
||||
true
|
||||
}
|
||||
R.id.action_edit_notification_filter -> {
|
||||
showFilterDialog()
|
||||
true
|
||||
}
|
||||
R.id.action_clear_notifications -> {
|
||||
confirmClearNotifications()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
|
@ -518,7 +514,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) {
|
||||
|
|
@ -621,7 +621,6 @@ class NotificationsFragment :
|
|||
|
||||
override fun onReselect() {
|
||||
if (isAdded) {
|
||||
binding.appBarOptions.setExpanded(true, false)
|
||||
layoutManager.scrollToPosition(0)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ class NotificationsPagingAdapter(
|
|||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
|
||||
return when (NotificationViewKind.values()[viewType]) {
|
||||
return when (NotificationViewKind.entries[viewType]) {
|
||||
NotificationViewKind.STATUS -> {
|
||||
StatusViewHolder(
|
||||
ItemStatusBinding.inflate(inflater, parent, false),
|
||||
|
|
|
|||
|
|
@ -46,7 +46,6 @@ import com.keylesspalace.tusky.util.toViewData
|
|||
import com.keylesspalace.tusky.viewdata.NotificationViewData
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
|
|
@ -70,29 +69,23 @@ import kotlinx.coroutines.rx3.await
|
|||
import retrofit2.HttpException
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.ExperimentalTime
|
||||
|
||||
data class UiState(
|
||||
/** Filtered notification types */
|
||||
val activeFilter: Set<Notification.Type> = emptySet(),
|
||||
|
||||
/** True if the UI to filter and clear notifications should be shown */
|
||||
val showFilterOptions: Boolean = false,
|
||||
|
||||
/** True if the FAB should be shown while scrolling */
|
||||
val showFabWhileScrolling: Boolean = true
|
||||
)
|
||||
|
||||
/** Preferences the UI reacts to */
|
||||
data class UiPrefs(
|
||||
val showFabWhileScrolling: Boolean,
|
||||
val showFilter: Boolean
|
||||
val showFabWhileScrolling: Boolean
|
||||
) {
|
||||
companion object {
|
||||
/** Relevant preference keys. Changes to any of these trigger a display update */
|
||||
val prefKeys = setOf(
|
||||
PrefKeys.FAB_HIDE,
|
||||
PrefKeys.SHOW_NOTIFICATIONS_FILTER
|
||||
PrefKeys.FAB_HIDE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -103,7 +96,7 @@ sealed class UiAction
|
|||
/** Actions the user can trigger from the UI. These actions may fail. */
|
||||
sealed class FallibleUiAction : UiAction() {
|
||||
/** Clear all notifications */
|
||||
object ClearNotifications : FallibleUiAction()
|
||||
data object ClearNotifications : FallibleUiAction()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -129,7 +122,7 @@ sealed class InfallibleUiAction : UiAction() {
|
|||
// Resets the account's `lastNotificationId`, which can't fail, which is why this is
|
||||
// infallible. Reloading the data may fail, but that's handled by the paging system /
|
||||
// adapter refresh logic.
|
||||
object LoadNewest : InfallibleUiAction()
|
||||
data object LoadNewest : InfallibleUiAction()
|
||||
}
|
||||
|
||||
/** Actions the user can trigger on an individual notification. These may fail. */
|
||||
|
|
@ -146,13 +139,13 @@ sealed class UiSuccess {
|
|||
// of these three should trigger the UI to refresh.
|
||||
|
||||
/** A user was blocked */
|
||||
object Block : UiSuccess()
|
||||
data object Block : UiSuccess()
|
||||
|
||||
/** A user was muted */
|
||||
object Mute : UiSuccess()
|
||||
data object Mute : UiSuccess()
|
||||
|
||||
/** A conversation was muted */
|
||||
object MuteConversation : UiSuccess()
|
||||
data object MuteConversation : UiSuccess()
|
||||
}
|
||||
|
||||
/** The result of a successful action on a notification */
|
||||
|
|
@ -286,7 +279,7 @@ sealed class UiError(
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class, ExperimentalTime::class)
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class NotificationsViewModel @Inject constructor(
|
||||
private val repository: NotificationsRepository,
|
||||
private val preferences: SharedPreferences,
|
||||
|
|
@ -497,7 +490,6 @@ class NotificationsViewModel @Inject constructor(
|
|||
uiState = combine(notificationFilter, getUiPrefs()) { filter, prefs ->
|
||||
UiState(
|
||||
activeFilter = filter.filter,
|
||||
showFilterOptions = prefs.showFilter,
|
||||
showFabWhileScrolling = prefs.showFabWhileScrolling
|
||||
)
|
||||
}.stateIn(
|
||||
|
|
@ -546,8 +538,7 @@ class NotificationsViewModel @Inject constructor(
|
|||
.onStart { emit(toPrefs()) }
|
||||
|
||||
private fun toPrefs() = UiPrefs(
|
||||
showFabWhileScrolling = !preferences.getBoolean(PrefKeys.FAB_HIDE, false),
|
||||
showFilter = preferences.getBoolean(PrefKeys.SHOW_NOTIFICATIONS_FILTER, true)
|
||||
showFabWhileScrolling = !preferences.getBoolean(PrefKeys.FAB_HIDE, false)
|
||||
)
|
||||
|
||||
companion object {
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import com.keylesspalace.tusky.appstore.EventHub
|
|||
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
|
||||
import com.keylesspalace.tusky.databinding.ActivityPreferencesBinding
|
||||
import com.keylesspalace.tusky.settings.PrefKeys
|
||||
import com.keylesspalace.tusky.settings.PrefKeys.APP_THEME
|
||||
import com.keylesspalace.tusky.util.APP_THEME_DEFAULT
|
||||
import com.keylesspalace.tusky.util.getNonNullString
|
||||
import com.keylesspalace.tusky.util.setAppNightMode
|
||||
|
|
@ -145,8 +146,8 @@ class PreferencesActivity :
|
|||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
|
||||
when (key) {
|
||||
"appTheme" -> {
|
||||
val theme = sharedPreferences.getNonNullString("appTheme", APP_THEME_DEFAULT)
|
||||
APP_THEME -> {
|
||||
val theme = sharedPreferences.getNonNullString(APP_THEME, APP_THEME_DEFAULT)
|
||||
Log.d("activeTheme", theme)
|
||||
setAppNightMode(theme)
|
||||
|
||||
|
|
|
|||
|
|
@ -208,13 +208,6 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
|||
isSingleLineTitle = false
|
||||
}
|
||||
|
||||
switchPreference {
|
||||
setDefaultValue(true)
|
||||
key = PrefKeys.SHOW_NOTIFICATIONS_FILTER
|
||||
setTitle(R.string.pref_title_show_notifications_filter)
|
||||
isSingleLineTitle = false
|
||||
}
|
||||
|
||||
switchPreference {
|
||||
setDefaultValue(true)
|
||||
key = PrefKeys.CONFIRM_REBLOGS
|
||||
|
|
|
|||
|
|
@ -19,9 +19,9 @@ import android.os.Bundle
|
|||
import androidx.preference.PreferenceFragmentCompat
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.settings.PrefKeys
|
||||
import com.keylesspalace.tusky.settings.checkBoxPreference
|
||||
import com.keylesspalace.tusky.settings.makePreferenceScreen
|
||||
import com.keylesspalace.tusky.settings.preferenceCategory
|
||||
import com.keylesspalace.tusky.settings.switchPreference
|
||||
|
||||
class TabFilterPreferencesFragment : PreferenceFragmentCompat() {
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
|
|
@ -29,14 +29,14 @@ class TabFilterPreferencesFragment : PreferenceFragmentCompat() {
|
|||
preferenceCategory(R.string.title_home) { category ->
|
||||
category.isIconSpaceReserved = false
|
||||
|
||||
checkBoxPreference {
|
||||
switchPreference {
|
||||
setTitle(R.string.pref_title_show_boosts)
|
||||
key = PrefKeys.TAB_FILTER_HOME_BOOSTS
|
||||
setDefaultValue(true)
|
||||
isIconSpaceReserved = false
|
||||
}
|
||||
|
||||
checkBoxPreference {
|
||||
switchPreference {
|
||||
setTitle(R.string.pref_title_show_replies)
|
||||
key = PrefKeys.TAB_FILTER_HOME_REPLIES
|
||||
setDefaultValue(true)
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ import dagger.android.DispatchingAndroidInjector
|
|||
import dagger.android.HasAndroidInjector
|
||||
import javax.inject.Inject
|
||||
|
||||
class SearchActivity : BottomSheetActivity(), HasAndroidInjector, MenuProvider {
|
||||
class SearchActivity : BottomSheetActivity(), HasAndroidInjector, MenuProvider, SearchView.OnQueryTextListener {
|
||||
@Inject
|
||||
lateinit var androidInjector: DispatchingAndroidInjector<Any>
|
||||
|
||||
|
|
@ -91,8 +91,6 @@ class SearchActivity : BottomSheetActivity(), HasAndroidInjector, MenuProvider {
|
|||
searchViewMenuItem.expandActionView()
|
||||
val searchView = searchViewMenuItem.actionView as SearchView
|
||||
setupSearchView(searchView)
|
||||
|
||||
searchView.setQuery(viewModel.currentQuery, false)
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||
|
|
@ -150,9 +148,23 @@ class SearchActivity : BottomSheetActivity(), HasAndroidInjector, MenuProvider {
|
|||
val pxBuffer = ((48 * 2) * resources.displayMetrics.density).toInt()
|
||||
searchView.maxWidth = pxScreenWidth - pxBuffer
|
||||
|
||||
// Keep text that was entered also when switching to a different tab (before the search is executed)
|
||||
searchView.setOnQueryTextListener(this)
|
||||
searchView.setQuery(viewModel.currentSearchFieldContent ?: "", false)
|
||||
|
||||
searchView.requestFocus()
|
||||
}
|
||||
|
||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(newText: String?): Boolean {
|
||||
viewModel.currentSearchFieldContent = newText
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
override fun androidInjector() = androidInjector
|
||||
|
||||
companion object {
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ class SearchViewModel @Inject constructor(
|
|||
) : ViewModel() {
|
||||
|
||||
var currentQuery: String = ""
|
||||
var currentSearchFieldContent: String? = null
|
||||
|
||||
val activeAccount: AccountEntity?
|
||||
get() = accountManager.activeAccount
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
package com.keylesspalace.tusky.components.timeline.util
|
||||
|
||||
import com.google.gson.JsonParseException
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
|
||||
fun Throwable.isExpected() = this is IOException || this is HttpException
|
||||
fun Throwable.isExpected() = this is IOException || this is HttpException || this is JsonParseException
|
||||
|
||||
inline fun <T> ifExpected(
|
||||
t: Throwable,
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
|
||||
package com.keylesspalace.tusky.components.timeline.viewmodel
|
||||
|
||||
import android.util.Log
|
||||
import androidx.paging.ExperimentalPagingApi
|
||||
import androidx.paging.LoadType
|
||||
import androidx.paging.PagingState
|
||||
|
|
@ -117,6 +118,7 @@ class CachedTimelineRemoteMediator(
|
|||
return MediatorResult.Success(endOfPaginationReached = statuses.isEmpty())
|
||||
} catch (e: Exception) {
|
||||
return ifExpected(e) {
|
||||
Log.w(TAG, "Failed to load timeline", e)
|
||||
MediatorResult.Error(e)
|
||||
}
|
||||
}
|
||||
|
|
@ -175,4 +177,8 @@ class CachedTimelineRemoteMediator(
|
|||
}
|
||||
return overlappedStatuses
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "CachedTimelineRM"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
|
||||
package com.keylesspalace.tusky.components.timeline.viewmodel
|
||||
|
||||
import android.util.Log
|
||||
import androidx.paging.ExperimentalPagingApi
|
||||
import androidx.paging.LoadType
|
||||
import androidx.paging.PagingState
|
||||
|
|
@ -106,8 +107,13 @@ class NetworkTimelineRemoteMediator(
|
|||
return MediatorResult.Success(endOfPaginationReached = statuses.isEmpty())
|
||||
} catch (e: Exception) {
|
||||
return ifExpected(e) {
|
||||
Log.w(TAG, "Failed to load timeline", e)
|
||||
MediatorResult.Error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "NetworkTimelineRM"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ class TrendingActivity : BaseActivity(), HasAndroidInjector {
|
|||
|
||||
if (supportFragmentManager.findFragmentById(R.id.fragmentContainer) == null) {
|
||||
supportFragmentManager.commit {
|
||||
val fragment = TrendingFragment.newInstance()
|
||||
val fragment = TrendingTagsFragment.newInstance()
|
||||
replace(R.id.fragmentContainer, fragment)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import com.keylesspalace.tusky.databinding.ItemTrendingCellBinding
|
|||
import com.keylesspalace.tusky.databinding.ItemTrendingDateBinding
|
||||
import com.keylesspalace.tusky.viewdata.TrendingViewData
|
||||
|
||||
class TrendingAdapter(
|
||||
class TrendingTagsAdapter(
|
||||
private val onViewTag: (String) -> Unit
|
||||
) : ListAdapter<TrendingViewData, RecyclerView.ViewHolder>(TrendingDifferCallback) {
|
||||
|
||||
|
|
@ -33,8 +33,8 @@ import at.connyduck.sparkbutton.helpers.Utils
|
|||
import com.keylesspalace.tusky.BaseActivity
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.StatusListActivity
|
||||
import com.keylesspalace.tusky.components.trending.viewmodel.TrendingViewModel
|
||||
import com.keylesspalace.tusky.databinding.FragmentTrendingBinding
|
||||
import com.keylesspalace.tusky.components.trending.viewmodel.TrendingTagsViewModel
|
||||
import com.keylesspalace.tusky.databinding.FragmentTrendingTagsBinding
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||
import com.keylesspalace.tusky.interfaces.ActionButtonActivity
|
||||
|
|
@ -48,8 +48,8 @@ import kotlinx.coroutines.flow.collectLatest
|
|||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class TrendingFragment :
|
||||
Fragment(R.layout.fragment_trending),
|
||||
class TrendingTagsFragment :
|
||||
Fragment(R.layout.fragment_trending_tags),
|
||||
OnRefreshListener,
|
||||
Injectable,
|
||||
ReselectableFragment,
|
||||
|
|
@ -58,11 +58,11 @@ class TrendingFragment :
|
|||
@Inject
|
||||
lateinit var viewModelFactory: ViewModelFactory
|
||||
|
||||
private val viewModel: TrendingViewModel by viewModels { viewModelFactory }
|
||||
private val viewModel: TrendingTagsViewModel by viewModels { viewModelFactory }
|
||||
|
||||
private val binding by viewBinding(FragmentTrendingBinding::bind)
|
||||
private val binding by viewBinding(FragmentTrendingTagsBinding::bind)
|
||||
|
||||
private val adapter = TrendingAdapter(::onViewTag)
|
||||
private val adapter = TrendingTagsAdapter(::onViewTag)
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
|
|
@ -111,8 +111,8 @@ class TrendingFragment :
|
|||
spanSizeLookup = object : SpanSizeLookup() {
|
||||
override fun getSpanSize(position: Int): Int {
|
||||
return when (adapter.getItemViewType(position)) {
|
||||
TrendingAdapter.VIEW_TYPE_HEADER -> columnCount
|
||||
TrendingAdapter.VIEW_TYPE_TAG -> 1
|
||||
TrendingTagsAdapter.VIEW_TYPE_HEADER -> columnCount
|
||||
TrendingTagsAdapter.VIEW_TYPE_TAG -> 1
|
||||
else -> -1
|
||||
}
|
||||
}
|
||||
|
|
@ -139,15 +139,15 @@ class TrendingFragment :
|
|||
(requireActivity() as BaseActivity).startActivityWithSlideInAnimation(StatusListActivity.newHashtagIntent(requireContext(), tag))
|
||||
}
|
||||
|
||||
private fun processViewState(uiState: TrendingViewModel.TrendingUiState) {
|
||||
private fun processViewState(uiState: TrendingTagsViewModel.TrendingTagsUiState) {
|
||||
Log.d(TAG, uiState.loadingState.name)
|
||||
when (uiState.loadingState) {
|
||||
TrendingViewModel.LoadingState.INITIAL -> clearLoadingState()
|
||||
TrendingViewModel.LoadingState.LOADING -> applyLoadingState()
|
||||
TrendingViewModel.LoadingState.REFRESHING -> applyRefreshingState()
|
||||
TrendingViewModel.LoadingState.LOADED -> applyLoadedState(uiState.trendingViewData)
|
||||
TrendingViewModel.LoadingState.ERROR_NETWORK -> networkError()
|
||||
TrendingViewModel.LoadingState.ERROR_OTHER -> otherError()
|
||||
TrendingTagsViewModel.LoadingState.INITIAL -> clearLoadingState()
|
||||
TrendingTagsViewModel.LoadingState.LOADING -> applyLoadingState()
|
||||
TrendingTagsViewModel.LoadingState.REFRESHING -> applyRefreshingState()
|
||||
TrendingTagsViewModel.LoadingState.LOADED -> applyLoadedState(uiState.trendingViewData)
|
||||
TrendingTagsViewModel.LoadingState.ERROR_NETWORK -> networkError()
|
||||
TrendingTagsViewModel.LoadingState.ERROR_OTHER -> otherError()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -194,7 +194,7 @@ class TrendingFragment :
|
|||
|
||||
binding.swipeRefreshLayout.isRefreshing = false
|
||||
binding.messageView.setup(
|
||||
R.drawable.elephant_offline,
|
||||
R.drawable.errorphant_offline,
|
||||
R.string.error_network
|
||||
) { refreshContent() }
|
||||
}
|
||||
|
|
@ -206,7 +206,7 @@ class TrendingFragment :
|
|||
|
||||
binding.swipeRefreshLayout.isRefreshing = false
|
||||
binding.messageView.setup(
|
||||
R.drawable.elephant_error,
|
||||
R.drawable.errorphant_error,
|
||||
R.string.error_generic
|
||||
) { refreshContent() }
|
||||
}
|
||||
|
|
@ -247,8 +247,8 @@ class TrendingFragment :
|
|||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "TrendingFragment"
|
||||
private const val TAG = "TrendingTagsFragment"
|
||||
|
||||
fun newInstance() = TrendingFragment()
|
||||
fun newInstance() = TrendingTagsFragment()
|
||||
}
|
||||
}
|
||||
|
|
@ -35,7 +35,7 @@ import kotlinx.coroutines.launch
|
|||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
class TrendingViewModel @Inject constructor(
|
||||
class TrendingTagsViewModel @Inject constructor(
|
||||
private val mastodonApi: MastodonApi,
|
||||
private val eventHub: EventHub
|
||||
) : ViewModel() {
|
||||
|
|
@ -43,13 +43,13 @@ class TrendingViewModel @Inject constructor(
|
|||
INITIAL, LOADING, REFRESHING, LOADED, ERROR_NETWORK, ERROR_OTHER
|
||||
}
|
||||
|
||||
data class TrendingUiState(
|
||||
data class TrendingTagsUiState(
|
||||
val trendingViewData: List<TrendingViewData>,
|
||||
val loadingState: LoadingState
|
||||
)
|
||||
|
||||
val uiState: Flow<TrendingUiState> get() = _uiState
|
||||
private val _uiState = MutableStateFlow(TrendingUiState(listOf(), LoadingState.INITIAL))
|
||||
val uiState: Flow<TrendingTagsUiState> get() = _uiState
|
||||
private val _uiState = MutableStateFlow(TrendingTagsUiState(listOf(), LoadingState.INITIAL))
|
||||
|
||||
init {
|
||||
invalidate()
|
||||
|
|
@ -73,38 +73,42 @@ class TrendingViewModel @Inject constructor(
|
|||
*/
|
||||
fun invalidate(refresh: Boolean = false) = viewModelScope.launch {
|
||||
if (refresh) {
|
||||
_uiState.value = TrendingUiState(emptyList(), LoadingState.REFRESHING)
|
||||
_uiState.value = TrendingTagsUiState(emptyList(), LoadingState.REFRESHING)
|
||||
} else {
|
||||
_uiState.value = TrendingUiState(emptyList(), LoadingState.LOADING)
|
||||
_uiState.value = TrendingTagsUiState(emptyList(), LoadingState.LOADING)
|
||||
}
|
||||
|
||||
val deferredFilters = async { mastodonApi.getFilters() }
|
||||
|
||||
mastodonApi.trendingTags().fold(
|
||||
{ tagResponse ->
|
||||
val homeFilters = deferredFilters.await().getOrNull()?.filter { filter ->
|
||||
filter.context.contains(Filter.Kind.HOME.kind)
|
||||
}
|
||||
val tags = tagResponse
|
||||
.filter { tag ->
|
||||
homeFilters?.none { filter ->
|
||||
filter.keywords.any { keyword -> keyword.keyword.equals(tag.name, ignoreCase = true) }
|
||||
} ?: false
|
||||
|
||||
val firstTag = tagResponse.firstOrNull()
|
||||
_uiState.value = if (firstTag == null) {
|
||||
TrendingTagsUiState(emptyList(), LoadingState.LOADED)
|
||||
} else {
|
||||
val homeFilters = deferredFilters.await().getOrNull()?.filter { filter ->
|
||||
filter.context.contains(Filter.Kind.HOME.kind)
|
||||
}
|
||||
.sortedByDescending { tag -> tag.history.sumOf { it.uses.toLongOrNull() ?: 0 } }
|
||||
.toViewData()
|
||||
val tags = tagResponse
|
||||
.filter { tag ->
|
||||
homeFilters?.none { filter ->
|
||||
filter.keywords.any { keyword -> keyword.keyword.equals(tag.name, ignoreCase = true) }
|
||||
} ?: false
|
||||
}
|
||||
.sortedByDescending { tag -> tag.history.sumOf { it.uses.toLongOrNull() ?: 0 } }
|
||||
.toViewData()
|
||||
|
||||
val firstTag = tagResponse.first()
|
||||
val header = TrendingViewData.Header(firstTag.start(), firstTag.end())
|
||||
|
||||
_uiState.value = TrendingUiState(listOf(header) + tags, LoadingState.LOADED)
|
||||
val header = TrendingViewData.Header(firstTag.start(), firstTag.end())
|
||||
TrendingTagsUiState(listOf(header) + tags, LoadingState.LOADED)
|
||||
}
|
||||
},
|
||||
{ error ->
|
||||
Log.w(TAG, "failed loading trending tags", error)
|
||||
if (error is IOException) {
|
||||
_uiState.value = TrendingUiState(emptyList(), LoadingState.ERROR_NETWORK)
|
||||
_uiState.value = TrendingTagsUiState(emptyList(), LoadingState.ERROR_NETWORK)
|
||||
} else {
|
||||
_uiState.value = TrendingUiState(emptyList(), LoadingState.ERROR_OTHER)
|
||||
_uiState.value = TrendingTagsUiState(emptyList(), LoadingState.ERROR_OTHER)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
@ -24,36 +24,34 @@ import androidx.core.view.forEach
|
|||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.keylesspalace.tusky.R
|
||||
|
||||
class ConversationLineItemDecoration(private val context: Context) : RecyclerView.ItemDecoration() {
|
||||
|
||||
class ConversationLineItemDecoration(context: Context) : RecyclerView.ItemDecoration() {
|
||||
private val divider: Drawable = ContextCompat.getDrawable(context, R.drawable.conversation_thread_line)!!
|
||||
|
||||
override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
|
||||
val dividerStart = parent.paddingStart + context.resources.getDimensionPixelSize(R.dimen.status_line_margin_start)
|
||||
val dividerEnd = dividerStart + divider.intrinsicWidth
|
||||
private val avatarTopMargin = context.resources.getDimensionPixelSize(R.dimen.account_avatar_margin)
|
||||
private val halfAvatarHeight = context.resources.getDimensionPixelSize(R.dimen.timeline_status_avatar_height) / 2
|
||||
private val statusLineMarginStart = context.resources.getDimensionPixelSize(R.dimen.status_line_margin_start)
|
||||
|
||||
val avatarMargin = context.resources.getDimensionPixelSize(R.dimen.account_avatar_margin)
|
||||
override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
|
||||
val dividerStart = parent.paddingStart + statusLineMarginStart
|
||||
val dividerEnd = dividerStart + divider.intrinsicWidth
|
||||
|
||||
val items = (parent.adapter as ThreadAdapter).currentList
|
||||
|
||||
parent.forEach { child ->
|
||||
parent.forEach { statusItemView ->
|
||||
val position = parent.getChildAdapterPosition(statusItemView)
|
||||
|
||||
val position = parent.getChildAdapterPosition(child)
|
||||
|
||||
val current = items.getOrNull(position)
|
||||
|
||||
if (current != null) {
|
||||
items.getOrNull(position)?.let { current ->
|
||||
val above = items.getOrNull(position - 1)
|
||||
val dividerTop = if (above != null && above.id == current.status.inReplyToId) {
|
||||
child.top
|
||||
statusItemView.top
|
||||
} else {
|
||||
child.top + avatarMargin
|
||||
statusItemView.top + avatarTopMargin + halfAvatarHeight
|
||||
}
|
||||
val below = items.getOrNull(position + 1)
|
||||
val dividerBottom = if (below != null && current.id == below.status.inReplyToId && !current.isDetailed) {
|
||||
child.bottom
|
||||
statusItemView.bottom
|
||||
} else {
|
||||
child.top + avatarMargin
|
||||
statusItemView.top + avatarTopMargin + halfAvatarHeight
|
||||
}
|
||||
|
||||
if (parent.layoutDirection == View.LAYOUT_DIRECTION_LTR) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -516,7 +516,7 @@ class ViewThreadViewModel @Inject constructor(
|
|||
|
||||
sealed interface ThreadUiState {
|
||||
/** The initial load of the detailed status for this thread */
|
||||
object Loading : ThreadUiState
|
||||
data object Loading : ThreadUiState
|
||||
|
||||
/** Loading the detailed status has completed, now loading ancestors/descendants */
|
||||
data class LoadingThread(
|
||||
|
|
@ -535,7 +535,7 @@ sealed interface ThreadUiState {
|
|||
) : ThreadUiState
|
||||
|
||||
/** Refreshing the thread with a swipe */
|
||||
object Refreshing : ThreadUiState
|
||||
data object Refreshing : ThreadUiState
|
||||
}
|
||||
|
||||
enum class RevealButtonState {
|
||||
|
|
|
|||
|
|
@ -51,10 +51,10 @@ class ViewEditsAdapter(
|
|||
private val absoluteTimeFormatter = AbsoluteTimeFormatter()
|
||||
|
||||
/** Size of large text in this theme, in px */
|
||||
var largeTextSizePx: Float = 0f
|
||||
private var largeTextSizePx: Float = 0f
|
||||
|
||||
/** Size of medium text in this theme, in px */
|
||||
var mediumTextSizePx: Float = 0f
|
||||
private var mediumTextSizePx: Float = 0f
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
|
|
|
|||
|
|
@ -132,12 +132,12 @@ class ViewEditsViewModel @Inject constructor(private val api: MastodonApi) : Vie
|
|||
}
|
||||
|
||||
sealed interface EditsUiState {
|
||||
object Initial : EditsUiState
|
||||
object Loading : EditsUiState
|
||||
data object Initial : EditsUiState
|
||||
data object Loading : EditsUiState
|
||||
|
||||
// "Refreshing" state is necessary, otherwise a refresh state transition is Success -> Success,
|
||||
// and state flows don't emit repeated states, so the UI never updates.
|
||||
object Refreshing : EditsUiState
|
||||
data object Refreshing : EditsUiState
|
||||
class Error(val throwable: Throwable) : EditsUiState
|
||||
data class Success(
|
||||
val edits: List<StatusEdit>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue