fix codestyle

This commit is contained in:
Konrad Pozniak 2021-06-28 22:04:34 +02:00
parent 9ca7e708bd
commit 2cc53d6772
7 changed files with 423 additions and 358 deletions

View File

@ -48,7 +48,12 @@ import com.bumptech.glide.request.transition.Transition
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayout.OnTabSelectedListener import com.google.android.material.tabs.TabLayout.OnTabSelectedListener
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import com.keylesspalace.tusky.appstore.* import com.keylesspalace.tusky.appstore.AnnouncementReadEvent
import com.keylesspalace.tusky.appstore.CacheUpdater
import com.keylesspalace.tusky.appstore.Event
import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.MainTabsChangedEvent
import com.keylesspalace.tusky.appstore.ProfileEditedEvent
import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity
import com.keylesspalace.tusky.components.compose.ComposeActivity import com.keylesspalace.tusky.components.compose.ComposeActivity
import com.keylesspalace.tusky.components.compose.ComposeActivity.Companion.canHandleMimeType import com.keylesspalace.tusky.components.compose.ComposeActivity.Companion.canHandleMimeType
@ -67,7 +72,14 @@ import com.keylesspalace.tusky.interfaces.ActionButtonActivity
import com.keylesspalace.tusky.interfaces.ReselectableFragment import com.keylesspalace.tusky.interfaces.ReselectableFragment
import com.keylesspalace.tusky.pager.MainPagerAdapter import com.keylesspalace.tusky.pager.MainPagerAdapter
import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.* import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.deleteStaleCachedMedia
import com.keylesspalace.tusky.util.emojify
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.removeShortcut
import com.keylesspalace.tusky.util.updateShortcut
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.util.visible
import com.mikepenz.iconics.IconicsDrawable 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
@ -76,9 +88,24 @@ import com.mikepenz.materialdrawer.holder.BadgeStyle
import com.mikepenz.materialdrawer.holder.ColorHolder import com.mikepenz.materialdrawer.holder.ColorHolder
import com.mikepenz.materialdrawer.holder.StringHolder import com.mikepenz.materialdrawer.holder.StringHolder
import com.mikepenz.materialdrawer.iconics.iconicsIcon import com.mikepenz.materialdrawer.iconics.iconicsIcon
import com.mikepenz.materialdrawer.model.* import com.mikepenz.materialdrawer.model.AbstractDrawerItem
import com.mikepenz.materialdrawer.model.interfaces.* import com.mikepenz.materialdrawer.model.DividerDrawerItem
import com.mikepenz.materialdrawer.util.* import com.mikepenz.materialdrawer.model.PrimaryDrawerItem
import com.mikepenz.materialdrawer.model.ProfileDrawerItem
import com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem
import com.mikepenz.materialdrawer.model.SecondaryDrawerItem
import com.mikepenz.materialdrawer.model.interfaces.IProfile
import com.mikepenz.materialdrawer.model.interfaces.descriptionRes
import com.mikepenz.materialdrawer.model.interfaces.descriptionText
import com.mikepenz.materialdrawer.model.interfaces.iconRes
import com.mikepenz.materialdrawer.model.interfaces.iconUrl
import com.mikepenz.materialdrawer.model.interfaces.nameRes
import com.mikepenz.materialdrawer.model.interfaces.nameText
import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader
import com.mikepenz.materialdrawer.util.DrawerImageLoader
import com.mikepenz.materialdrawer.util.addItems
import com.mikepenz.materialdrawer.util.addItemsAtPosition
import com.mikepenz.materialdrawer.util.updateBadge
import com.mikepenz.materialdrawer.widget.AccountHeaderView import com.mikepenz.materialdrawer.widget.AccountHeaderView
import dagger.android.DispatchingAndroidInjector import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector import dagger.android.HasAndroidInjector
@ -156,19 +183,22 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
forwardShare(intent) forwardShare(intent)
} else { } else {
// No account was provided, show the chooser // No account was provided, show the chooser
showAccountChooserDialog(getString(R.string.action_share_as), true, object : AccountSelectionListener { showAccountChooserDialog(
override fun onAccountSelected(account: AccountEntity) { getString(R.string.action_share_as), true,
val requestedId = account.id object : AccountSelectionListener {
if (requestedId == activeAccount.id) { override fun onAccountSelected(account: AccountEntity) {
// The correct account is already active val requestedId = account.id
forwardShare(intent) if (requestedId == activeAccount.id) {
} else { // The correct account is already active
// A different account was requested, restart the activity forwardShare(intent)
intent.putExtra(NotificationHelper.ACCOUNT_ID, requestedId) } else {
changeAccount(requestedId, intent) // A different account was requested, restart the activity
intent.putExtra(NotificationHelper.ACCOUNT_ID, requestedId)
changeAccount(requestedId, intent)
}
} }
} }
}) )
} }
} else if (accountRequested && savedInstanceState == null) { } else if (accountRequested && savedInstanceState == null) {
// user clicked a notification, show notification tab // user clicked a notification, show notification tab
@ -323,12 +353,15 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
headerBackgroundScaleType = ImageView.ScaleType.CENTER_CROP headerBackgroundScaleType = ImageView.ScaleType.CENTER_CROP
currentHiddenInList = true currentHiddenInList = true
onAccountHeaderListener = { _: View?, profile: IProfile, current: Boolean -> handleProfileClick(profile, current) } onAccountHeaderListener = { _: View?, profile: IProfile, current: Boolean -> handleProfileClick(profile, current) }
addProfile(ProfileSettingDrawerItem().apply { addProfile(
identifier = DRAWER_ITEM_ADD_ACCOUNT ProfileSettingDrawerItem().apply {
nameRes = R.string.add_account_name identifier = DRAWER_ITEM_ADD_ACCOUNT
descriptionRes = R.string.add_account_description nameRes = R.string.add_account_name
iconicsIcon = GoogleMaterial.Icon.gmd_add descriptionRes = R.string.add_account_description
}, 0) iconicsIcon = GoogleMaterial.Icon.gmd_add
},
0
)
attachToSliderView(binding.mainDrawer) attachToSliderView(binding.mainDrawer)
dividerBelowHeader = false dividerBelowHeader = false
closeDrawerOnProfileListClick = true closeDrawerOnProfileListClick = true
@ -468,14 +501,16 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
) )
if (addSearchButton) { if (addSearchButton) {
binding.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
onClick = { onClick = {
startActivityWithSlideInAnimation(SearchActivity.getIntent(context)) startActivityWithSlideInAnimation(SearchActivity.getIntent(context))
} }
}) }
)
} }
setSavedInstance(savedInstanceState) setSavedInstance(savedInstanceState)
@ -572,24 +607,23 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
binding.mainToolbar.setOnClickListener { binding.mainToolbar.setOnClickListener {
(adapter.getFragment(activeTabLayout.selectedTabPosition) as? ReselectableFragment)?.onReselect() (adapter.getFragment(activeTabLayout.selectedTabPosition) as? ReselectableFragment)?.onReselect()
} }
} }
private fun handleProfileClick(profile: IProfile, current: Boolean): Boolean { private fun handleProfileClick(profile: IProfile, current: Boolean): Boolean {
val activeAccount = accountManager.activeAccount val activeAccount = accountManager.activeAccount
//open profile when active image was clicked // open profile when active image was clicked
if (current && activeAccount != null) { if (current && activeAccount != null) {
val intent = AccountActivity.getIntent(this, activeAccount.accountId) val intent = AccountActivity.getIntent(this, activeAccount.accountId)
startActivityWithSlideInAnimation(intent) startActivityWithSlideInAnimation(intent)
return false return false
} }
//open LoginActivity to add new account // open LoginActivity to add new account
if (profile.identifier == DRAWER_ITEM_ADD_ACCOUNT) { if (profile.identifier == DRAWER_ITEM_ADD_ACCOUNT) {
startActivityWithSlideInAnimation(LoginActivity.getIntent(this, true)) startActivityWithSlideInAnimation(LoginActivity.getIntent(this, true))
return false return false
} }
//change Account // change Account
changeAccount(profile.identifier, null) changeAccount(profile.identifier, null)
return false return false
} }
@ -638,130 +672,130 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
finishWithoutSlideOutAnimation() finishWithoutSlideOutAnimation()
} }
} }
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
.show() .show()
}
}
private fun fetchUserInfo() {
mastodonApi.accountVerifyCredentials()
.observeOn(AndroidSchedulers.mainThread())
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
.subscribe(
{ userInfo ->
onFetchUserInfoSuccess(userInfo)
},
{ throwable ->
Log.e(TAG, "Failed to fetch user info. " + throwable.message)
}
)
}
private fun onFetchUserInfoSuccess(me: Account) {
glide.asBitmap()
.load(me.header)
.into(header.accountHeaderBackground)
loadDrawerAvatar(me.avatar, false)
accountManager.updateActiveAccount(me)
NotificationHelper.createNotificationChannelsForAccount(accountManager.activeAccount!!, this)
accountLocked = me.locked
updateProfiles()
updateShortcut(this, accountManager.activeAccount!!)
}
private fun loadDrawerAvatar(avatarUrl: String, showPlaceholder: Boolean) {
val navIconSize = resources.getDimensionPixelSize(R.dimen.avatar_toolbar_nav_icon_size)
glide.asDrawable()
.load(avatarUrl)
.transform(
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp))
)
.apply {
if (showPlaceholder) {
placeholder(R.drawable.avatar_default)
}
}
.into(object : CustomTarget<Drawable>(navIconSize, navIconSize) {
override fun onLoadStarted(placeholder: Drawable?) {
if (placeholder != null) {
binding.mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize)
}
}
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
binding.mainToolbar.navigationIcon = FixedSizeDrawable(resource, navIconSize, navIconSize)
}
override fun onLoadCleared(placeholder: Drawable?) {
if (placeholder != null) {
binding.mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize)
}
}
})
}
private fun fetchAnnouncements() {
mastodonApi.listAnnouncements(false)
.observeOn(AndroidSchedulers.mainThread())
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
.subscribe(
{ announcements ->
unreadAnnouncementsCount = announcements.count { !it.read }
updateAnnouncementsBadge()
},
{
Log.w(TAG, "Failed to fetch announcements.", it)
}
)
}
private fun updateAnnouncementsBadge() {
binding.mainDrawer.updateBadge(DRAWER_ITEM_ANNOUNCEMENTS, StringHolder(if (unreadAnnouncementsCount <= 0) null else unreadAnnouncementsCount.toString()))
}
private fun updateProfiles() {
val animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
val profiles: MutableList<IProfile> = accountManager.getAllAccountsOrderedByActive().map { acc ->
val emojifiedName = EmojiCompat.get().process(acc.displayName.emojify(acc.emojis, header, animateEmojis))
ProfileDrawerItem().apply {
isSelected = acc.isActive
nameText = emojifiedName
iconUrl = acc.profilePictureUrl
isNameShown = true
identifier = acc.id
descriptionText = acc.fullName
}
}.toMutableList()
// reuse the already existing "add account" item
for (profile in header.profiles.orEmpty()) {
if (profile.identifier == DRAWER_ITEM_ADD_ACCOUNT) {
profiles.add(profile)
break
} }
} }
header.clear()
header.profiles = profiles
header.setActiveProfile(accountManager.activeAccount!!.id)
}
override fun getActionButton() = binding.composeButton private fun fetchUserInfo() {
mastodonApi.accountVerifyCredentials()
.observeOn(AndroidSchedulers.mainThread())
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
.subscribe(
{ userInfo ->
onFetchUserInfoSuccess(userInfo)
},
{ throwable ->
Log.e(TAG, "Failed to fetch user info. " + throwable.message)
}
)
}
override fun androidInjector() = androidInjector private fun onFetchUserInfoSuccess(me: Account) {
glide.asBitmap()
.load(me.header)
.into(header.accountHeaderBackground)
companion object { loadDrawerAvatar(me.avatar, false)
private const val TAG = "MainActivity" // logging tag
private const val DRAWER_ITEM_ADD_ACCOUNT: Long = -13 accountManager.updateActiveAccount(me)
private const val DRAWER_ITEM_ANNOUNCEMENTS: Long = 14 NotificationHelper.createNotificationChannelsForAccount(accountManager.activeAccount!!, this)
const val STATUS_URL = "statusUrl"
} accountLocked = me.locked
updateProfiles()
updateShortcut(this, accountManager.activeAccount!!)
}
private fun loadDrawerAvatar(avatarUrl: String, showPlaceholder: Boolean) {
val navIconSize = resources.getDimensionPixelSize(R.dimen.avatar_toolbar_nav_icon_size)
glide.asDrawable()
.load(avatarUrl)
.transform(
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp))
)
.apply {
if (showPlaceholder) {
placeholder(R.drawable.avatar_default)
}
}
.into(object : CustomTarget<Drawable>(navIconSize, navIconSize) {
override fun onLoadStarted(placeholder: Drawable?) {
if (placeholder != null) {
binding.mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize)
}
}
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
binding.mainToolbar.navigationIcon = FixedSizeDrawable(resource, navIconSize, navIconSize)
}
override fun onLoadCleared(placeholder: Drawable?) {
if (placeholder != null) {
binding.mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize)
}
}
})
}
private fun fetchAnnouncements() {
mastodonApi.listAnnouncements(false)
.observeOn(AndroidSchedulers.mainThread())
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
.subscribe(
{ announcements ->
unreadAnnouncementsCount = announcements.count { !it.read }
updateAnnouncementsBadge()
},
{
Log.w(TAG, "Failed to fetch announcements.", it)
}
)
}
private fun updateAnnouncementsBadge() {
binding.mainDrawer.updateBadge(DRAWER_ITEM_ANNOUNCEMENTS, StringHolder(if (unreadAnnouncementsCount <= 0) null else unreadAnnouncementsCount.toString()))
}
private fun updateProfiles() {
val animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
val profiles: MutableList<IProfile> = accountManager.getAllAccountsOrderedByActive().map { acc ->
val emojifiedName = EmojiCompat.get().process(acc.displayName.emojify(acc.emojis, header, animateEmojis))
ProfileDrawerItem().apply {
isSelected = acc.isActive
nameText = emojifiedName
iconUrl = acc.profilePictureUrl
isNameShown = true
identifier = acc.id
descriptionText = acc.fullName
}
}.toMutableList()
// reuse the already existing "add account" item
for (profile in header.profiles.orEmpty()) {
if (profile.identifier == DRAWER_ITEM_ADD_ACCOUNT) {
profiles.add(profile)
break
}
}
header.clear()
header.profiles = profiles
header.setActiveProfile(accountManager.activeAccount!!.id)
}
override fun getActionButton() = binding.composeButton
override fun androidInjector() = androidInjector
companion object {
private const val TAG = "MainActivity" // logging tag
private const val DRAWER_ITEM_ADD_ACCOUNT: Long = -13
private const val DRAWER_ITEM_ANNOUNCEMENTS: Long = 14
const val STATUS_URL = "statusUrl"
}
} }
private inline fun primaryDrawerItem(block: PrimaryDrawerItem.() -> Unit): PrimaryDrawerItem { private inline fun primaryDrawerItem(block: PrimaryDrawerItem.() -> Unit): PrimaryDrawerItem {

View File

@ -28,26 +28,36 @@ import com.keylesspalace.tusky.components.search.SearchType
import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.db.InstanceEntity import com.keylesspalace.tusky.db.InstanceEntity
import com.keylesspalace.tusky.entity.* import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.entity.NewPoll
import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.service.ServiceClient import com.keylesspalace.tusky.service.ServiceClient
import com.keylesspalace.tusky.service.TootToSend import com.keylesspalace.tusky.service.TootToSend
import com.keylesspalace.tusky.util.* import com.keylesspalace.tusky.util.Either
import com.keylesspalace.tusky.util.RxAwareViewModel
import com.keylesspalace.tusky.util.VersionUtils
import com.keylesspalace.tusky.util.combineLiveData
import com.keylesspalace.tusky.util.filter
import com.keylesspalace.tusky.util.map
import com.keylesspalace.tusky.util.randomAlphanumericString
import com.keylesspalace.tusky.util.toLiveData
import com.keylesspalace.tusky.util.withoutFirstWhich
import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.disposables.Disposable
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.* import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
class ComposeViewModel @Inject constructor( class ComposeViewModel @Inject constructor(
private val api: MastodonApi, private val api: MastodonApi,
private val accountManager: AccountManager, private val accountManager: AccountManager,
private val mediaUploader: MediaUploader, private val mediaUploader: MediaUploader,
private val serviceClient: ServiceClient, private val serviceClient: ServiceClient,
private val draftHelper: DraftHelper, private val draftHelper: DraftHelper,
private val db: AppDatabase private val db: AppDatabase
) : RxAwareViewModel() { ) : RxAwareViewModel() {
private var replyingStatusAuthor: String? = null private var replyingStatusAuthor: String? = null
@ -66,15 +76,15 @@ class ComposeViewModel @Inject constructor(
val instanceParams: LiveData<ComposeInstanceParams> = instance.map { instance -> val instanceParams: LiveData<ComposeInstanceParams> = instance.map { instance ->
ComposeInstanceParams( ComposeInstanceParams(
maxChars = instance?.maximumTootCharacters ?: DEFAULT_CHARACTER_LIMIT, maxChars = instance?.maximumTootCharacters ?: DEFAULT_CHARACTER_LIMIT,
pollMaxOptions = instance?.maxPollOptions ?: DEFAULT_MAX_OPTION_COUNT, pollMaxOptions = instance?.maxPollOptions ?: DEFAULT_MAX_OPTION_COUNT,
pollMaxLength = instance?.maxPollOptionLength ?: DEFAULT_MAX_OPTION_LENGTH, pollMaxLength = instance?.maxPollOptionLength ?: DEFAULT_MAX_OPTION_LENGTH,
supportsScheduled = instance?.version?.let { VersionUtils(it).supportsScheduledToots() } ?: false supportsScheduled = instance?.version?.let { VersionUtils(it).supportsScheduledToots() } ?: false
) )
} }
val emoji: MutableLiveData<List<Emoji>?> = MutableLiveData() val emoji: MutableLiveData<List<Emoji>?> = MutableLiveData()
val markMediaAsSensitive = val markMediaAsSensitive =
mutableLiveData(accountManager.activeAccount?.defaultMediaSensitivity ?: false) mutableLiveData(accountManager.activeAccount?.defaultMediaSensitivity ?: false)
val statusVisibility = mutableLiveData(Status.Visibility.UNKNOWN) val statusVisibility = mutableLiveData(Status.Visibility.UNKNOWN)
val showContentWarning = mutableLiveData(false) val showContentWarning = mutableLiveData(false)
@ -91,30 +101,36 @@ class ComposeViewModel @Inject constructor(
init { init {
Single.zip(api.getCustomEmojis(), api.getInstance(), { emojis, instance -> Single.zip(
InstanceEntity( api.getCustomEmojis(), api.getInstance(),
{ emojis, instance ->
InstanceEntity(
instance = accountManager.activeAccount?.domain!!, instance = accountManager.activeAccount?.domain!!,
emojiList = emojis, emojiList = emojis,
maximumTootCharacters = instance.maxTootChars, maximumTootCharacters = instance.maxTootChars,
maxPollOptions = instance.pollLimits?.maxOptions, maxPollOptions = instance.pollLimits?.maxOptions,
maxPollOptionLength = instance.pollLimits?.maxOptionChars, maxPollOptionLength = instance.pollLimits?.maxOptionChars,
version = instance.version version = instance.version
) )
}) }
.doOnSuccess { )
db.instanceDao().insertOrReplace(it) .doOnSuccess {
} db.instanceDao().insertOrReplace(it)
.onErrorResumeNext { }
db.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!) .onErrorResumeNext {
} db.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!)
.subscribe({ instanceEntity -> }
.subscribe(
{ instanceEntity ->
emoji.postValue(instanceEntity.emojiList) emoji.postValue(instanceEntity.emojiList)
instance.postValue(instanceEntity) instance.postValue(instanceEntity)
}, { throwable -> },
{ throwable ->
// this can happen on network error when no cached data is available // this can happen on network error when no cached data is available
Log.w(TAG, "error loading instance data", throwable) Log.w(TAG, "error loading instance data", throwable)
}) }
.autoDispose() )
.autoDispose()
} }
fun pickMedia(uri: Uri, description: String? = null): LiveData<Either<Throwable, QueuedMedia>> { fun pickMedia(uri: Uri, description: String? = null): LiveData<Either<Throwable, QueuedMedia>> {
@ -122,44 +138,49 @@ class ComposeViewModel @Inject constructor(
// the Activity goes away temporarily (like on screen rotation). // the Activity goes away temporarily (like on screen rotation).
val liveData = MutableLiveData<Either<Throwable, QueuedMedia>>() val liveData = MutableLiveData<Either<Throwable, QueuedMedia>>()
mediaUploader.prepareMedia(uri) mediaUploader.prepareMedia(uri)
.map { (type, uri, size) -> .map { (type, uri, size) ->
val mediaItems = media.value!! val mediaItems = media.value!!
if (type != QueuedMedia.Type.IMAGE if (type != QueuedMedia.Type.IMAGE &&
&& mediaItems.isNotEmpty() mediaItems.isNotEmpty() &&
&& mediaItems[0].type == QueuedMedia.Type.IMAGE) { mediaItems[0].type == QueuedMedia.Type.IMAGE
throw VideoOrImageException() ) {
} else { throw VideoOrImageException()
addMediaToQueue(type, uri, size, description) } else {
} addMediaToQueue(type, uri, size, description)
} }
.subscribe({ queuedMedia -> }
.subscribe(
{ queuedMedia ->
liveData.postValue(Either.Right(queuedMedia)) liveData.postValue(Either.Right(queuedMedia))
}, { error -> },
{ error ->
liveData.postValue(Either.Left(error)) liveData.postValue(Either.Left(error))
}) }
.autoDispose() )
.autoDispose()
return liveData return liveData
} }
private fun addMediaToQueue( private fun addMediaToQueue(
type: QueuedMedia.Type, type: QueuedMedia.Type,
uri: Uri, uri: Uri,
mediaSize: Long, mediaSize: Long,
description: String? = null description: String? = null
): QueuedMedia { ): QueuedMedia {
val mediaItem = QueuedMedia( val mediaItem = QueuedMedia(
localId = System.currentTimeMillis(), localId = System.currentTimeMillis(),
uri = uri, uri = uri,
type = type, type = type,
mediaSize = mediaSize, mediaSize = mediaSize,
description = description description = description
) )
media.value = media.value!! + mediaItem media.value = media.value!! + mediaItem
mediaToDisposable[mediaItem.localId] = mediaUploader mediaToDisposable[mediaItem.localId] = mediaUploader
.uploadMedia(mediaItem) .uploadMedia(mediaItem)
.subscribe({ event -> .subscribe(
{ event ->
val item = media.value?.find { it.localId == mediaItem.localId } val item = media.value?.find { it.localId == mediaItem.localId }
?: return@subscribe ?: return@subscribe
val newMediaItem = when (event) { val newMediaItem = when (event) {
is UploadEvent.ProgressEvent -> is UploadEvent.ProgressEvent ->
item.copy(uploadPercent = event.percentage) item.copy(uploadPercent = event.percentage)
@ -169,16 +190,20 @@ class ComposeViewModel @Inject constructor(
synchronized(media) { synchronized(media) {
val mediaValue = media.value!! val mediaValue = media.value!!
val index = mediaValue.indexOfFirst { it.localId == newMediaItem.localId } val index = mediaValue.indexOfFirst { it.localId == newMediaItem.localId }
media.postValue(if (index == -1) { media.postValue(
mediaValue + newMediaItem if (index == -1) {
} else { mediaValue + newMediaItem
mediaValue.toMutableList().also { it[index] = newMediaItem } } else {
}) mediaValue.toMutableList().also { it[index] = newMediaItem }
}
)
} }
}, { error -> },
{ error ->
media.postValue(media.value?.filter { it.localId != mediaItem.localId } ?: emptyList()) media.postValue(media.value?.filter { it.localId != mediaItem.localId } ?: emptyList())
uploadError.postValue(error) uploadError.postValue(error)
}) }
)
return mediaItem return mediaItem
} }
@ -198,12 +223,14 @@ class ComposeViewModel @Inject constructor(
fun didChange(content: String?, contentWarning: String?): Boolean { fun didChange(content: String?, contentWarning: String?): Boolean {
val textChanged = !(content.isNullOrEmpty() val textChanged = !(
|| startingText?.startsWith(content.toString()) ?: false) content.isNullOrEmpty() ||
startingText?.startsWith(content.toString()) ?: false
)
val contentWarningChanged = showContentWarning.value!! val contentWarningChanged = showContentWarning.value!! &&
&& !contentWarning.isNullOrEmpty() !contentWarning.isNullOrEmpty() &&
&& !startingContentWarning.startsWith(contentWarning.toString()) !startingContentWarning.startsWith(contentWarning.toString())
val mediaChanged = !media.value.isNullOrEmpty() val mediaChanged = !media.value.isNullOrEmpty()
val pollChanged = poll.value != null val pollChanged = poll.value != null
@ -254,8 +281,8 @@ class ComposeViewModel @Inject constructor(
* @return LiveData which will signal once the screen can be closed or null if there are errors * @return LiveData which will signal once the screen can be closed or null if there are errors
*/ */
fun sendStatus( fun sendStatus(
content: String, content: String,
spoilerText: String spoilerText: String
): LiveData<Unit> { ): LiveData<Unit> {
val deletionObservable = if (isEditingScheduledToot) { val deletionObservable = if (isEditingScheduledToot) {
@ -265,39 +292,39 @@ class ComposeViewModel @Inject constructor(
}.toLiveData() }.toLiveData()
val sendObservable = media val sendObservable = media
.filter { items -> items.all { it.uploadPercent == -1 } } .filter { items -> items.all { it.uploadPercent == -1 } }
.map { .map {
val mediaIds = ArrayList<String>() val mediaIds = ArrayList<String>()
val mediaUris = ArrayList<Uri>() val mediaUris = ArrayList<Uri>()
val mediaDescriptions = ArrayList<String>() val mediaDescriptions = ArrayList<String>()
for (item in media.value!!) { for (item in media.value!!) {
mediaIds.add(item.id!!) mediaIds.add(item.id!!)
mediaUris.add(item.uri) mediaUris.add(item.uri)
mediaDescriptions.add(item.description ?: "") mediaDescriptions.add(item.description ?: "")
}
val tootToSend = TootToSend(
text = content,
warningText = spoilerText,
visibility = statusVisibility.value!!.serverString(),
sensitive = mediaUris.isNotEmpty() && (markMediaAsSensitive.value!! || showContentWarning.value!!),
mediaIds = mediaIds,
mediaUris = mediaUris.map { it.toString() },
mediaDescriptions = mediaDescriptions,
scheduledAt = scheduledAt.value,
inReplyToId = inReplyToId,
poll = poll.value,
replyingStatusContent = null,
replyingStatusAuthorUsername = null,
accountId = accountManager.activeAccount!!.id,
draftId = draftId,
idempotencyKey = randomAlphanumericString(16),
retries = 0
)
serviceClient.sendToot(tootToSend)
} }
val tootToSend = TootToSend(
text = content,
warningText = spoilerText,
visibility = statusVisibility.value!!.serverString(),
sensitive = mediaUris.isNotEmpty() && (markMediaAsSensitive.value!! || showContentWarning.value!!),
mediaIds = mediaIds,
mediaUris = mediaUris.map { it.toString() },
mediaDescriptions = mediaDescriptions,
scheduledAt = scheduledAt.value,
inReplyToId = inReplyToId,
poll = poll.value,
replyingStatusContent = null,
replyingStatusAuthorUsername = null,
accountId = accountManager.activeAccount!!.id,
draftId = draftId,
idempotencyKey = randomAlphanumericString(16),
retries = 0
)
serviceClient.sendToot(tootToSend)
}
return combineLiveData(deletionObservable, sendObservable) { _, _ -> } return combineLiveData(deletionObservable, sendObservable) { _, _ -> }
} }
@ -316,12 +343,15 @@ class ComposeViewModel @Inject constructor(
media.removeObserver(this) media.removeObserver(this)
} else if (updatedItem.id != null) { } else if (updatedItem.id != null) {
api.updateMedia(updatedItem.id, description) api.updateMedia(updatedItem.id, description)
.subscribe({ .subscribe(
{
completedCaptioningLiveData.postValue(true) completedCaptioningLiveData.postValue(true)
}, { },
{
completedCaptioningLiveData.postValue(false) completedCaptioningLiveData.postValue(false)
}) }
.autoDispose() )
.autoDispose()
media.removeObserver(this) media.removeObserver(this)
} }
} }
@ -334,8 +364,8 @@ class ComposeViewModel @Inject constructor(
'@' -> { '@' -> {
return try { return try {
api.searchAccounts(query = token.substring(1), limit = 10) api.searchAccounts(query = token.substring(1), limit = 10)
.blockingGet() .blockingGet()
.map { ComposeAutoCompleteAdapter.AccountResult(it) } .map { ComposeAutoCompleteAdapter.AccountResult(it) }
} catch (e: Throwable) { } catch (e: Throwable) {
Log.e(TAG, String.format("Autocomplete search for %s failed.", token), e) Log.e(TAG, String.format("Autocomplete search for %s failed.", token), e)
emptyList() emptyList()
@ -344,9 +374,9 @@ class ComposeViewModel @Inject constructor(
'#' -> { '#' -> {
return try { return try {
api.searchObservable(query = token, type = SearchType.Hashtag.apiParameter, limit = 10) api.searchObservable(query = token, type = SearchType.Hashtag.apiParameter, limit = 10)
.blockingGet() .blockingGet()
.hashtags .hashtags
.map { ComposeAutoCompleteAdapter.HashtagResult(it) } .map { ComposeAutoCompleteAdapter.HashtagResult(it) }
} catch (e: Throwable) { } catch (e: Throwable) {
Log.e(TAG, String.format("Autocomplete search for %s failed.", token), e) Log.e(TAG, String.format("Autocomplete search for %s failed.", token), e)
emptyList() emptyList()
@ -389,7 +419,8 @@ class ComposeViewModel @Inject constructor(
val replyVisibility = composeOptions?.replyVisibility ?: Status.Visibility.UNKNOWN val replyVisibility = composeOptions?.replyVisibility ?: Status.Visibility.UNKNOWN
startingVisibility = Status.Visibility.byNum( startingVisibility = Status.Visibility.byNum(
preferredVisibility.num.coerceAtLeast(replyVisibility.num)) preferredVisibility.num.coerceAtLeast(replyVisibility.num)
)
inReplyToId = composeOptions?.inReplyToId inReplyToId = composeOptions?.inReplyToId
@ -468,7 +499,6 @@ class ComposeViewModel @Inject constructor(
private companion object { private companion object {
const val TAG = "ComposeViewModel" const val TAG = "ComposeViewModel"
} }
} }
fun <T> mutableLiveData(default: T) = MutableLiveData<T>().apply { value = default } fun <T> mutableLiveData(default: T) = MutableLiveData<T>().apply { value = default }
@ -478,10 +508,10 @@ private const val DEFAULT_MAX_OPTION_COUNT = 4
private const val DEFAULT_MAX_OPTION_LENGTH = 25 private const val DEFAULT_MAX_OPTION_LENGTH = 25
data class ComposeInstanceParams( data class ComposeInstanceParams(
val maxChars: Int, val maxChars: Int,
val pollMaxOptions: Int, val pollMaxOptions: Int,
val pollMaxLength: Int, val pollMaxLength: Int,
val supportsScheduled: Boolean val supportsScheduled: Boolean
) )
/** /**

View File

@ -37,83 +37,83 @@ import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
class DraftHelper @Inject constructor( class DraftHelper @Inject constructor(
val context: Context, val context: Context,
db: AppDatabase db: AppDatabase
) { ) {
private val draftDao = db.draftDao() private val draftDao = db.draftDao()
suspend fun saveDraft( suspend fun saveDraft(
draftId: Int, draftId: Int,
accountId: Long, accountId: Long,
inReplyToId: String?, inReplyToId: String?,
content: String?, content: String?,
contentWarning: String?, contentWarning: String?,
sensitive: Boolean, sensitive: Boolean,
visibility: Status.Visibility, visibility: Status.Visibility,
mediaUris: List<String>, mediaUris: List<String>,
mediaDescriptions: List<String?>, mediaDescriptions: List<String?>,
poll: NewPoll?, poll: NewPoll?,
failedToSend: Boolean failedToSend: Boolean
) = withContext(Dispatchers.IO) { ) = withContext(Dispatchers.IO) {
val externalFilesDir = context.getExternalFilesDir("Tusky") val externalFilesDir = context.getExternalFilesDir("Tusky")
if (externalFilesDir == null || !(externalFilesDir.exists())) { if (externalFilesDir == null || !(externalFilesDir.exists())) {
Log.e("DraftHelper", "Error obtaining directory to save media.") Log.e("DraftHelper", "Error obtaining directory to save media.")
throw Exception() throw Exception()
}
val draftDirectory = File(externalFilesDir, "Drafts")
if (!draftDirectory.exists()) {
draftDirectory.mkdir()
}
val uris = mediaUris.map { uriString ->
uriString.toUri()
}.map { uri ->
if (uri.isNotInFolder(draftDirectory)) {
uri.copyToFolder(draftDirectory)
} else {
uri
} }
}
val draftDirectory = File(externalFilesDir, "Drafts") val types = uris.map { uri ->
val mimeType = context.contentResolver.getType(uri)
if (!draftDirectory.exists()) { when (mimeType?.substring(0, mimeType.indexOf('/'))) {
draftDirectory.mkdir() "video" -> DraftAttachment.Type.VIDEO
"image" -> DraftAttachment.Type.IMAGE
"audio" -> DraftAttachment.Type.AUDIO
else -> throw IllegalStateException("unknown media type")
} }
}
val uris = mediaUris.map { uriString -> val attachments: MutableList<DraftAttachment> = mutableListOf()
uriString.toUri() for (i in mediaUris.indices) {
}.map { uri -> attachments.add(
if (uri.isNotInFolder(draftDirectory)) { DraftAttachment(
uri.copyToFolder(draftDirectory) uriString = uris[i].toString(),
} else { description = mediaDescriptions[i],
uri type = types[i]
}
}
val types = uris.map { uri ->
val mimeType = context.contentResolver.getType(uri)
when (mimeType?.substring(0, mimeType.indexOf('/'))) {
"video" -> DraftAttachment.Type.VIDEO
"image" -> DraftAttachment.Type.IMAGE
"audio" -> DraftAttachment.Type.AUDIO
else -> throw IllegalStateException("unknown media type")
}
}
val attachments: MutableList<DraftAttachment> = mutableListOf()
for (i in mediaUris.indices) {
attachments.add(
DraftAttachment(
uriString = uris[i].toString(),
description = mediaDescriptions[i],
type = types[i]
)
) )
}
val draft = DraftEntity(
id = draftId,
accountId = accountId,
inReplyToId = inReplyToId,
content = content,
contentWarning = contentWarning,
sensitive = sensitive,
visibility = visibility,
attachments = attachments,
poll = poll,
failedToSend = failedToSend
) )
}
draftDao.insertOrReplace(draft) val draft = DraftEntity(
id = draftId,
accountId = accountId,
inReplyToId = inReplyToId,
content = content,
contentWarning = contentWarning,
sensitive = sensitive,
visibility = visibility,
attachments = attachments,
poll = poll,
failedToSend = failedToSend
)
draftDao.insertOrReplace(draft)
} }
suspend fun deleteDraftAndAttachments(draftId: Int) { suspend fun deleteDraftAndAttachments(draftId: Int) {
@ -162,5 +162,4 @@ class DraftHelper @Inject constructor(
IOUtils.copyToFile(contentResolver, this, file) IOUtils.copyToFile(contentResolver, this, file)
return FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider", file) return FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider", file)
} }
}
}

View File

@ -34,17 +34,17 @@ interface DraftActionListener {
} }
class DraftsAdapter( class DraftsAdapter(
private val listener: DraftActionListener private val listener: DraftActionListener
) : PagingDataAdapter<DraftEntity, BindingHolder<ItemDraftBinding>>( ) : PagingDataAdapter<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
}
override fun areContentsTheSame(oldItem: DraftEntity, newItem: DraftEntity): Boolean {
return oldItem == newItem
}
} }
override fun areContentsTheSame(oldItem: DraftEntity, newItem: DraftEntity): Boolean {
return oldItem == newItem
}
}
) { ) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemDraftBinding> { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemDraftBinding> {

View File

@ -93,7 +93,7 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec
} }
if (loadState.refresh is LoadState.NotLoading) { if (loadState.refresh is LoadState.NotLoading) {
binding.progressBar.hide() binding.progressBar.hide()
if(adapter.itemCount == 0) { if (adapter.itemCount == 0) {
binding.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)
binding.errorMessageView.show() binding.errorMessageView.show()
} else { } else {
@ -117,16 +117,19 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec
} }
override fun edit(item: ScheduledStatus) { override fun edit(item: ScheduledStatus) {
val intent = ComposeActivity.startIntent(this, ComposeActivity.ComposeOptions( val intent = ComposeActivity.startIntent(
scheduledTootId = item.id, this,
tootText = item.params.text, ComposeActivity.ComposeOptions(
contentWarning = item.params.spoilerText, scheduledTootId = item.id,
mediaAttachments = item.mediaAttachments, tootText = item.params.text,
inReplyToId = item.params.inReplyToId, contentWarning = item.params.spoilerText,
visibility = item.params.visibility, mediaAttachments = item.mediaAttachments,
scheduledAt = item.scheduledAt, inReplyToId = item.params.inReplyToId,
sensitive = item.params.sensitive visibility = item.params.visibility,
)) scheduledAt = item.scheduledAt,
sensitive = item.params.sensitive
)
)
startActivity(intent) startActivity(intent)
} }

View File

@ -30,18 +30,17 @@ interface ScheduledTootActionListener {
} }
class ScheduledTootAdapter( class ScheduledTootAdapter(
val listener: ScheduledTootActionListener val listener: ScheduledTootActionListener
) : PagingDataAdapter<ScheduledStatus, BindingHolder<ItemScheduledTootBinding>>( ) : PagingDataAdapter<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
}
override fun areContentsTheSame(oldItem: ScheduledStatus, newItem: ScheduledStatus): Boolean {
return oldItem == newItem
}
} }
override fun areContentsTheSame(oldItem: ScheduledStatus, newItem: ScheduledStatus): Boolean {
return oldItem == newItem
}
}
) { ) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemScheduledTootBinding> { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemScheduledTootBinding> {
@ -50,7 +49,7 @@ class ScheduledTootAdapter(
} }
override fun onBindViewHolder(holder: BindingHolder<ItemScheduledTootBinding>, position: Int) { override fun onBindViewHolder(holder: BindingHolder<ItemScheduledTootBinding>, position: Int) {
getItem(position)?.let{ item -> getItem(position)?.let { item ->
holder.binding.edit.isEnabled = true holder.binding.edit.isEnabled = true
holder.binding.delete.isEnabled = true holder.binding.delete.isEnabled = true
holder.binding.text.text = item.params.text holder.binding.text.text = item.params.text

View File

@ -24,7 +24,7 @@ import kotlinx.coroutines.rx3.await
class ScheduledTootPagingSourceFactory( class ScheduledTootPagingSourceFactory(
private val mastodonApi: MastodonApi private val mastodonApi: MastodonApi
): () -> ScheduledTootPagingSource { ) : () -> ScheduledTootPagingSource {
private val scheduledTootsCache = mutableListOf<ScheduledStatus>() private val scheduledTootsCache = mutableListOf<ScheduledStatus>()
@ -45,7 +45,7 @@ class ScheduledTootPagingSourceFactory(
class ScheduledTootPagingSource( class ScheduledTootPagingSource(
private val mastodonApi: MastodonApi, private val mastodonApi: MastodonApi,
private val scheduledTootsCache: MutableList<ScheduledStatus> private val scheduledTootsCache: MutableList<ScheduledStatus>
): PagingSource<String, ScheduledStatus>() { ) : PagingSource<String, ScheduledStatus>() {
override fun getRefreshKey(state: PagingState<String, ScheduledStatus>): String? { override fun getRefreshKey(state: PagingState<String, ScheduledStatus>): String? {
return null return null