fix codestyle
This commit is contained in:
parent
9ca7e708bd
commit
2cc53d6772
7 changed files with 423 additions and 358 deletions
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Reference in a new issue